Compare commits

..

102 Commits

Author SHA1 Message Date
147e13bb26 More fixes 2024-01-05 17:28:17 -06:00
faa54108fa Second attempt 2024-01-05 17:13:02 -06:00
44c1dbe6e0 Fix portable creation 2024-01-05 17:04:51 -06:00
01563af114 Updated Changelog and release notes 2024-01-05 16:39:19 -06:00
cdcbcf754a Mastodon: Added actions for notifications. closes #517 2024-01-05 15:49:18 -06:00
3907777c91 fix: Handle empty notifications 2024-01-05 11:13:37 -06:00
fa0b6a63b9 Change: Updated text for some menu items on posts' contextual menu 2024-01-05 09:40:04 -06:00
9cfeacbd5a Core: Removed files no longer needed for TWBlue distributions 2023-12-31 20:08:58 -06:00
08f6ee7a1b Actions: Removed --draft flag for new releases 2023-12-31 20:03:14 -06:00
3e571c82d1 Update Readme 2023-12-31 19:51:00 -06:00
e835274ce9 Mastodon: TWBlue should be able to ignore deleted messages, so it will load everything else properly 2023-12-31 14:45:05 -06:00
8b5c47da28 Core: Updated website references for TWBlue 2023-12-31 12:20:06 -06:00
47271cd34d Core: Updated changelog and release info 2023-12-31 11:57:40 -06:00
2919c3b851 Actions: Typo fix 2023-12-31 00:51:36 -06:00
8388899481 Actions: Write new version info without the leading 'v' 2023-12-31 00:42:47 -06:00
acc17170f9 Actions: prepare a draft release 2023-12-31 00:36:06 -06:00
6f2642914c Actions: Get release version from pushed tag and use Release notes file to generate releases 2023-12-31 00:34:05 -06:00
ae56f879a8 Actions: Added Release Notes file 2023-12-31 00:33:30 -06:00
f8d3dfc37f Actions: Attempt to set TWBlue version based in pushed tag 2023-12-31 00:30:31 -06:00
3a867f0b82 Run new release workflow on pushed tags 2023-12-31 00:05:07 -06:00
2aff1076cb Merge branch 'next-gen' of github.com:mcv-software/twblue into next-gen 2023-12-31 00:03:56 -06:00
e0a4bd6a5d Core: Update information will be retrieved from GitHub repo only 2023-12-31 00:03:30 -06:00
manuelcortez
6e230b1216 Updated translation catalogs 2023-12-31 00:55:06 +00:00
19e18bcfe1 Merge pull request #567 from Arfs6/create-github-releases-action
Created release.yml - action file for automated release on push.
2023-12-30 11:11:21 -06:00
Abdulqadir Ahmad
6e91c6419c changed the command that creates a new release in the release workflow to create a general note and dropped the -p (pre release) flag. 2023-12-30 16:52:09 +01:00
Abdulqadir Ahmad
275f5e763b changed release workflow from releasing snapshot versions to "stable" versions. More info at https://github.com/MCV-Software/TWBlue/pull/567 2023-12-30 16:40:19 +01:00
Abdulqadir Ahmad
8d93c170e2 changed release.yml to run on pushes on next-gen branch 2023-12-29 19:01:14 +01:00
Abdulqadir Ahmad
7d66fa0695 Created release.yml - action file for automated release on push. 2023-12-29 18:10:26 +01:00
701c509cf4 Merge pull request #565 from Arfs6/profile-dialog-fixes
Show Profile dialog fixes
2023-12-27 12:32:50 -06:00
manuelcortez
4b988755e4 Updated translation catalogs 2023-12-24 00:55:03 +00:00
manuelcortez
1e8c893313 Updated translation catalogs 2023-12-17 00:55:13 +00:00
manuelcortez
6692d21269 Updated translation catalogs 2023-12-10 00:55:11 +00:00
José Manuel Delicado Alcolea
1547e14ed5 Updated version on updates.json to match current stable version. This should prevent unwanted behaviour on the updater when the website is offline 2023-12-07 23:03:59 +01:00
manuelcortez
712fd6574c Updated translation catalogs 2023-12-03 00:55:05 +00:00
Abdulqadir Ahmad
32718f5865 Merge branch 'next-gen' into profile-dialog-fixes
Merged updates from next-gen
2023-12-02 09:41:46 +01:00
Abdulqadir Ahmad
8c3fc2bbb8 updated show user profile dialog to download images in a separate thread 2023-12-02 08:50:57 +01:00
Abdulqadir Ahmad
80df3f254b Removed number of rows in grid sizer of show user profile dialog 2023-12-02 06:56:58 +01:00
manuelcortez
ce12eaad7b Updated translation catalogs 2023-11-26 00:55:04 +00:00
manuelcortez
6408d533e0 Updated translation catalogs 2023-11-19 00:55:06 +00:00
manuelcortez
a49003b340 Updated translation catalogs 2023-11-12 00:54:57 +00:00
manuelcortez
04e848b18c Updated translation catalogs 2023-11-05 00:55:09 +00:00
manuelcortez
3499162ffb Updated translation catalogs 2023-10-29 00:54:58 +00:00
manuelcortez
614de0c8e2 Updated translation catalogs 2023-10-22 00:54:57 +00:00
Riku
2993331f63 Translated using Weblate (Japanese)
Currently translated at 100.0% (727 of 727 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/ja/
2023-10-15 22:58:39 -05:00
manuelcortez
691f49fc04 Updated translation catalogs 2023-10-15 00:55:03 +00:00
6c43c419fe Mastodon: Added autocomplete users management in account settings; user autocomplete works already in posts 2023-10-11 11:44:43 -06:00
14f48e4bbe Mastodon: Added user autocompletion module (not yet implemented on GUI) 2023-10-11 11:29:39 -06:00
a5dba08b06 Updated changelog 2023-10-10 17:56:40 -06:00
d56c8b4372 Renamed showUserProfile to user_details to keep compatibility with invisible interface and naming pattern of functions 2023-10-10 17:43:31 -06:00
b3e0b21ee7 Merge pull request #540 from Arfs6/stop_update_running_source
stop updating while running from source
2023-10-10 16:50:11 -06:00
e6ad42de48 Merge pull request #555 from Arfs6/create-show-user-profile
created show user profile dialogh
2023-10-10 16:48:01 -06:00
manuelcortez
abb97448b0 Updated translation catalogs 2023-10-08 00:54:55 +00:00
manuelcortez
371a8969e6 Updated translation catalogs 2023-10-01 00:55:14 +00:00
manuelcortez
91e91d0e8d Updated translation catalogs 2023-09-24 00:55:00 +00:00
manuelcortez
ea42f5a1b2 Updated translation catalogs 2023-09-17 00:54:54 +00:00
manuelcortez
c1987c60e1 Updated translation catalogs 2023-09-10 00:54:48 +00:00
manuelcortez
312ec050ac Updated translation catalogs 2023-09-03 00:54:47 +00:00
Riku
c82f94c636 Translated using Weblate (Japanese)
Currently translated at 100.0% (684 of 684 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/ja/
2023-08-27 20:51:33 -05:00
Abdulqadir Ahmad
f4ec03099a added actions, following, followers and posts button to show user profile dialog 2023-08-27 17:56:39 +01:00
manuelcortez
1aea675fa5 Updated translation catalogs 2023-08-27 00:54:53 +00:00
Corentin Bacqué-Cazenave
f005352da0 Translated using Weblate (French)
Currently translated at 100.0% (684 of 684 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/fr/
2023-08-24 06:50:22 -05:00
manuelcortez
06d6e9a15b Updated translation catalogs 2023-08-20 00:54:47 +00:00
manuelcortez
9c37c32633 Updated translation catalogs 2023-08-13 00:54:56 +00:00
Abdulqadir Ahmad
15e2032afb added fields for created_at, locked, bot and discoverable 2023-08-12 10:32:29 +01:00
Abdulqadir Ahmad
35ba915be6 fix show user profile not working when a post have mentions 2023-08-10 19:15:34 +01:00
Abdulqadir Ahmad
1d8fefe7d3 created show user profile dialog 2023-08-10 17:19:58 +01:00
manuelcortez
d78335407a Updated translation catalogs 2023-08-06 00:49:23 +00:00
manuelcortez
f42ea96ce8 Updated translation catalogs 2023-07-30 00:55:18 +00:00
manuelcortez
a71bb716b2 Updated translation catalogs 2023-07-23 00:56:10 +00:00
manuelcortez
98c728cf48 Updated translation catalogs 2023-07-16 00:55:52 +00:00
53af099300 Mastodon: Create searches for posts during startup 2023-07-10 12:53:24 -06:00
7c5a41791c Mastodon: Added missing method to search buffer and removed some unneeded code 2023-07-10 12:52:42 -06:00
2cd90e8df1 Merge branch 'Arfs6-create_search_buffer' into next-gen 2023-07-10 09:59:27 -06:00
ceee7775c8 Fix conflicts 2023-07-10 09:58:08 -06:00
manuelcortez
4a256414d6 Updated translation catalogs 2023-07-09 00:56:01 +00:00
manuelcortez
94f48aa7fc Updated translation catalogs 2023-07-02 00:55:47 +00:00
1963c13b48 Merge pull request #547 from Arfs6/create_update_profile
Create update profile
2023-06-26 17:47:32 -06:00
manuelcortez
bb0edfdc65 Updated translation catalogs 2023-06-25 00:56:20 +00:00
Nikola Jović
23453ae6ab Translated using Weblate (Serbian)
Currently translated at 100.0% (667 of 667 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/sr/
2023-06-21 17:45:45 -05:00
Corentin Bacqué-Cazenave
bade40e8d7 Translated using Weblate (French)
Currently translated at 100.0% (667 of 667 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/fr/
2023-06-19 14:45:45 -05:00
manuelcortez
3080361275 Updated translation catalogs 2023-06-18 00:56:39 +00:00
manuelcortez
013bceeb64 Updated translation catalogs 2023-06-11 00:55:56 +00:00
Abdulqadir Ahmad
c31bf3f1b2 increased size of text boxes for better visual.push 2023-06-09 15:28:19 +01:00
Abdulqadir Ahmad
f5815d7911 Added check boxes for bot, private and discoverable 2023-06-09 14:38:22 +01:00
Abdulqadir Ahmad
6d7f808196 added support for updating header, avatar and the 4 fields 2023-06-09 10:53:11 +01:00
Abdulqadir Ahmad
453630e655 created basic update profile with support for name and bio 2023-06-08 00:40:38 +01:00
manuelcortez
61525023ce Updated translation catalogs 2023-06-04 00:56:16 +00:00
Abdulqadir Ahmad
f9b54ede81 fix updating while running from source if user checks for update from menu 2023-06-03 00:39:42 +01:00
Abdulqadir Ahmad
f0d6e8dcf9 created search buffer 2023-06-01 15:48:53 +01:00
Abdulqadir Ahmad
288286f21e now tw blue doesn't ask for updates when running from source 2023-05-30 09:26:29 +01:00
manuelcortez
6bd0161818 Updated translation catalogs 2023-05-28 00:55:31 +00:00
manuelcortez
5a7a5363ea Updated translation catalogs 2023-05-21 00:55:16 +00:00
manuelcortez
08efeeb5ed Updated translation catalogs 2023-05-14 00:55:21 +00:00
manuelcortez
2499b75bc2 Updated translation catalogs 2023-05-07 00:55:16 +00:00
Riku
7cfabca63d Translated using Weblate (Japanese)
Currently translated at 100.0% (667 of 667 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/ja/
2023-05-04 21:45:28 -05:00
manuelcortez
a00047b8a3 Updated translation catalogs 2023-04-30 00:55:38 +00:00
manuelcortez
805e33c44d Updated translation catalogs 2023-04-23 00:56:00 +00:00
zvonimir stanecic
283d4d9317 Translated using Weblate (Polish)
Currently translated at 100.0% (667 of 667 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/pl/
2023-04-21 02:45:24 -05:00
manuelcortez
b437e46101 Updated translation catalogs 2023-04-16 00:55:11 +00:00
Nikola Jović
47681d7c9d Translated using Weblate (Serbian)
Currently translated at 100.0% (667 of 667 strings)

Translation: TWBlue/TWBlue
Translate-URL: https://weblate.mcvsoftware.com/projects/twblue/twblue/sr/
2023-04-14 14:45:23 -05:00
bea4096d16 Remove keys directory from setup file 2023-04-13 12:55:43 -06:00
6135412d4b Removed twitter config include from setup script 2023-04-13 12:38:46 -06:00
97 changed files with 12013 additions and 6414 deletions

49
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
# Release a new TW Blue installer on github.
# This workflow runs on push.
name: Release
on:
push:
tags:
- v20*
workflow_dispatch:
jobs:
build:
# Builds an x64 binary and an installer of TW Blue.
runs-on: windows-latest
steps:
- name: clone repo
uses: actions/checkout@v4
with:
submodules: true
- name: Get python interpreter
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install python packages
run: python -m pip install -r requirements.txt
- name: Build binary
run: |
.\scripts\build.ps1
mv src/dist scripts\TWBlue64
- name: make installer
run: |
cd scripts
makensis twblue.nsi
- name: Create portable
working-directory: scripts\TWBlue64
run: |
7z a -tzip TWBlue_portable.zip .
- name: Create new release
env:
gh_token: ${{ github.token }}
run: |
mkdir .release-assets
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
gh release create release --draft -F "release-notes.md" -t "${{github.ref_name}}" .release-assets\TWBlue_setup_${{github.ref_name}}.exe .release-assets\TWBlue_portable_${{github.ref_name}}.zip

3
.gitignore vendored
View File

@@ -20,4 +20,5 @@ release-snapshot/
src/com_cache/
doc/strings.py
doc/changelog.py
env/
env/
version.txt

View File

@@ -1,164 +0,0 @@
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:
- build
- make_installer
- upload
twblue32:
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.10.8 -y -ForceX86
- '&$env:PYTHON -V'
- '&$env:PYTHON -m pip install --upgrade pip'
- '&$env:PYTHON -m pip install --upgrade https://github.com/josephsl/wxpy32whl/blob/main/wxPython-4.2.0-cp310-cp310-win32.whl?raw=true'
- '&$env:PYTHON -m pip install --upgrade -r requirements.txt'
stage: build
interruptible: true
script:
# Create html documentation firstly.
- cd doc
- '&$env:PYTHON documentation_importer.py'
- cd ..\src
- '&$env:PYTHON ..\doc\generator.py'
- '&$env:PYTHON write_version_data.py'
- '&$env:PYTHON setup.py build'
- cd ..
- mkdir artifacts
- cd scripts
- '&$env:PYTHON make_archive.py'
- cd ..
- mv src/dist artifacts/TWBlue
- move src/twblue.zip artifacts/twblue_x86.zip
# Move the generated script nsis file to artifacts, so we won't need python when generating the installer.
- move scripts/twblue.nsi artifacts/twblue.nsi
only:
- tags
artifacts:
paths:
- artifacts
expire_in: 1 day
twblue64:
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.10.8 -y
- '&$env:PYTHON -V'
- '&$env:PYTHON -m pip install --upgrade pip'
- '&$env:PYTHON -m pip install --upgrade -r requirements.txt'
stage: build
interruptible: true
script:
# Create html documentation firstly.
- cd doc
- '&$env:PYTHON documentation_importer.py'
- 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
- cd scripts
- '&$env:PYTHON make_archive.py'
- cd ..
- mv src/dist artifacts/TWBlue64
- move src/twblue.zip artifacts/twblue_x64.zip
only:
- tags
artifacts:
paths:
- 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:
- 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 nsis -y -ForceX86
script:
- move artifacts/TWBlue scripts/
- move artifacts/TWBlue64 scripts/
- move artifacts/twblue.nsi scripts/installer.nsi
- cd scripts
- '&$env:NSIS installer.nsi'
- move twblue_setup.exe ../artifacts
only:
- tags
artifacts:
paths:
- artifacts
expire_in: 1 day
upload:
stage: upload
tags:
- linux
image: python
interruptible: true
script:
- cd artifacts
- python ../scripts/upload.py
only:
- tags
- schedules

View File

@@ -1,11 +1,11 @@
TWBlue
======
[![Build status](https://ci.appveyor.com/api/projects/status/fml5fu7h1fj8vf6l?svg=true)](https://ci.appveyor.com/project/manuelcortez/twblue)
![Release status badge](https://github.com/mcv-software/twblue/actions/workflows/release.yml/badge.svg)
TWBlue is a free and open source application that allows you to interact with the main features of mastodon from the comfort of a windows software, with 2 different interfaces specially designed for screen reader users.
See [TWBlue's webpage](https://twblue.es) for more details.
See [TWBlue's webpage](https://twblue.mcvsoftware.com) for more details.
## Running TWBlue from source
@@ -22,7 +22,6 @@ Although most dependencies can be found in the windows-dependencies directory, w
#### Dependencies packaged in windows installers
* [Python,](https://python.org) version 3.10.8
If you want to build both x86 and x64 binaries, you can install python x64 to C:\python310 and python x86 to C:\python310-32, for example.
#### Dependencies that must be installed using pip
@@ -33,10 +32,6 @@ Python installs a tool called Pip that allows to install packages in a simple wa
You can also add the scripts folder to your path environment variable or choose the corresponding option when installing Python.
Note: pip and setuptools are included in the Python installer since version 2.7.9.
Note: If you are using Python for 32-bit systems, you will need to install WXPython for 32-bits before running the command for installing everything else. You can do so by running the following command:
`pip install --upgrade https://github.com/josephsl/wxpy32whl/blob/main/wxPython-4.2.0-cp310-cp310-win32.whl?raw=true`
Pip is able to install packages listed in a special text file, called the requirements file. To install all remaining dependencies, perform the following command:
`pip install -r requirements.txt`
@@ -74,7 +69,7 @@ Now that you have installed all these packages, you can run TW Blue from source
`python main.py`
If necessary, change the first part of the command to reflect the location of your python executable. You can run TW Blue using python x86 and x64.
If necessary, change the first part of the command to reflect the location of your python executable.
### Generating the documentation
@@ -100,10 +95,8 @@ To build it, run the following command from the src folder:
If you want to install TWBlue on your computer, you must create the installer first. Follow these steps:
* Navigate to the src directory, and create a binary version for x86: C:\python37\python setup.py build
* Move the dist directory to the scripts folder in this repo, and rename it to twblue
* Repeat these steps with Python for x64: C:\python37x64\python setup.py build
* Move the new dist directory to the scripts folder, and rename it to twblue64
* Navigate to the src directory, and create a binary version: C:\python310\python setup.py build
* Move the dist directory to the scripts folder in this repo, and rename it to twblue64
* Go to the scripts folder, right click on the twblue.nsi file, and choose compyle unicode NSIS script
* This may take a while. After the process, you will find the installer in the scripts folder
@@ -111,6 +104,8 @@ If you want to install TWBlue on your computer, you must create the installer fi
To manage translations in TWBlue, you can install the [Babel package.](https://pypi.org/project/Babel/) You can extract message catalogs and generate the main template file with the following command:
```bash
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.

View File

@@ -2,6 +2,24 @@ TWBlue Changelog
## changes in this version
* 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:
* 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))
* Implemented update profile dialog. ([#547](https://github.com/MCV-Software/TWBlue/pull/547))
* 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))
* Added possibility to vote in polls. This is mapped to Alt+Win+Shift+V in the invisible keymaps for windows 10/11.
* 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.
## changes in version 2023.4.13
During the development of the current TWBlue version, Twitter has cut out access from their API, meaning TWBlue will no longer be able to communicate with Twitter. This is the end of the support of TWBlue for Twitter sessions. No new sessions will be available for this social network, and we will focus in adding more features to our Mastodon support and writing support for more websites and networks. Thank you everyone who have been using TWBlue to manage your Twitter accounts since 2013.
* TWBlue should be able to display variables within templates (for example, now it is possible to send a template inside a post's text). Before, it was removing $variables so it was difficult to show how to edit templates from the client. ([#515](https://github.com/MCV-Software/TWBlue/issues/515))
@@ -15,7 +33,7 @@ During the development of the current TWBlue version, Twitter has cut out access
* Fixed an error on mentions buffer that was making TWBlue unable to load posts if there were mentions from a blocked or deleted account.
* Fixed an error when loading timelines during startup where TWBlue was unable to change the buffer title properly.
## Changes on version 2023.2.6
## Changes on version 2023.2.8
This release focuses on fixing some important bugs that have been reported in the previous version. Particularly, TWBlue should be able to authorize on some instances that have blocked the Mastodon.py library, and should be able to avoid repeatedly calling some endpoints that cause excessive connections for some instances. Additionally, it is possible to disable Streaming from the account options in Mastodon. This can be especially useful if TWBlue keeps making a lot of API calls for some instances.

View File

@@ -178,7 +178,7 @@ Visually, Towards the top of the main application window, can be found a menu ba
* Sounds tutorial: Opens a dialog box where you can familiarize yourself with the different sounds of the program.
* What's new in this version?: opens up a document with the list of changes from the current version to the earliest.
* Check for updates: every time you open the program it automatically checks for new versions. If an update is available, it will ask you if you want to download the update. If you accept, the updating process will commence. When complete, TWBlue will be restarted. This item checks for new updates without having to restart the application.
* TWBlue's website: visit our [home page](http://twblue.es) where you can find all relevant information and downloads for TWBlue and become a part of the community.
* TWBlue's website: visit our [home page](https://twblue.mcvsoftware.com) where you can find all relevant information and downloads for TWBlue and become a part of the community.
* Get soundpacks for TWBlue:
* Make a Donation: Opens a website from which you can donate to the TWBlue project. Donations are made through paypal and you don't need an account to donate.
* About TWBlue: shows the credits of the program.
@@ -322,11 +322,11 @@ Tw Blue is free software, licensed under the GNU GPL license, either version 2 o
The source code of the program is available on GitHub at <https://www.github.com/manuelcortez/twblue>.
If you want to donate to the project, you can do so at <https://twblue.es/donate>. Thank you for your support!
If you want to donate to the project, you can do so at <https://twblue.mcvsoftware.com/donate>. Thank you for your support!
## Contact
If you still have questions after reading this document, if you wish to collaborate to the project in some other way, or if you simply want to get in touch with the application developer, follow the Twitter account [@tw\_blue2](https://twitter.com/tw_blue2) or [@manuelcortez00.](https://twitter.com/manuelcortez00) You can also visit [our website](https://twblue.es)
If you still have questions after reading this document, if you wish to collaborate to the project in some other way, or if you simply want to get in touch with the application developer, follow the Twitter account [@tw\_blue2](https://twitter.com/tw_blue2) or [@manuelcortez00.](https://twitter.com/manuelcortez00) You can also visit [our website](https://twblue.mcvsoftware.com)
## Credits

View File

@@ -1,10 +0,0 @@
#include "api_keys.h"
char *get_api_key(){
return "key\0";
}
char *get_api_secret(){
return "secret_key\0";
}
char *get_twishort_api_key(){
return "key\0";
}

View File

@@ -1,8 +0,0 @@
#ifndef _API_KEYS_H
#define API_KEYS_H
char *get_api_key();
char *get_api_secret();
char *get_twishort_api_key();
#endif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1,8 +0,0 @@
[Launch]
ProgramExecutable=TWBlue\TWBlue.exe
ProgramExecutable64=TWBlue64\TWBlue.exe
CommandLineArguments=-d "%PAL:DataDir%"
SinglePortableAppInstance=true
MinOS=XP
SingleAppInstance=false
DirectoryMoveOK=yes

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 B

View File

@@ -1,28 +0,0 @@
[Format]
Type=PortableApps.comFormat
Version=3.5
[Details]
Name=tw blue portable
AppID=TWBluePortable
Publisher=jmdaweb & TW blue & PortableApps.com
Homepage=PortableApps.com/TWBluePortable
Category=Internet
Description=A portable, fast and accessible Twitter client with many options.
Language=Multilingual
InstallType=Multilingual
[License]
Shareable=true
OpenSource=true
Freeware=true
CommercialUse=true
EULAVersion=2
[Version]
PackageVersion=0.95.0.0
DisplayVersion=0.95
[Control]
Icons=1
Start=TWBluePortable.exe

View File

@@ -1,339 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -1,21 +0,0 @@
[Languages]
ENGLISH=true
ENGLISHGB=true
ARABIC=true
BASQUE=true
CATALAN=true
FINNISH=true
FRENCH=true
GALICIAN=true
GERMAN=true
CROATIAN=true
HUNGARIAN=true
ITALIAN=true
JAPANESE=true
POLISH=true
PORTUGUESEBR=true
ROMANIAN=true
RUSSIAN=true
SERBIAN=true
SPANISHINTERNATIONAL=true
TURKISH=true

View File

@@ -1,3 +0,0 @@
The files in this directory are necessary for the portable application to
function. There is normally no need to directly access or alter any of the
files within these directories.

View File

@@ -1,339 +0,0 @@
 GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,339 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -1,41 +0,0 @@
You can get tw blue's source code at https://github.com/manuelcortez/TWBlue
LICENSE
=======
This package's installer and launcher are released under the GPL. The launcher
is the PortableApps.com Launcher, available with full source and documentation
from http://portableapps.com/development. We request that developers using the
PortableApps.com Launcher please leave this directory intact and unchanged.
USER CONFIGURATION
==================
Some configuration in the PortableApps.com Launcher can be overridden by the
user in an INI file next to TWBluePortable.exe called TWBluePortable.ini.
If you are happy with the default options, it is not necessary, though. There
is an example INI included with this package to get you started. To use it,
copy AppNamePortable.ini from this directory to TWBluePortable.ini next to
TWBluePortable.exe. The options in the INI file are as follows:
AdditionalParameters=
DisableSplashScreen=false
RunLocally=false
(There is no need for an INI header in this file; if you have one, though, it
won't damage anything.)
The AdditionalParameters entry allows you to pass additional command-line
parameters to the application.
The DisableSplashScreen entry allows you to run the launcher without the splash
screen showing up. The default is false.
The RunLocally entry allows you to run the portable application from a read-
only medium. This is known as Live mode. It copies what it needs to to a
temporary directory on the host computer, runs the application, and then
deletes it afterwards, leaving nothing behind. This can be useful for running
the application from a CD or if you work on a computer that may have spyware or
viruses and you'd like to keep your device set to read-only. As a consequence
of this technique, any changes you make during the Live mode session aren't
saved back to your device. The default is false.

View File

@@ -1,6 +0,0 @@
AdditionalParameters=
DisableSplashScreen=false
RunLocally=false
# The above options are explained in the included readme.txt
# This INI file is an example only and is not used unless it is placed as described in the included readme.txt

View File

@@ -1,150 +0,0 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>tw blue Portable Help</title>
<link rel="alternate" href="http://portableapps.com/feeds/general" type="application/rss+xml" title="PortableApps.com">
<link rel="shortcut icon" href="Other/Help/Images/Favicon.ico">
<style type="text/css">
body {
font-family : Verdana,Arial,Helvetica,sans-serif;
font-size : 76%;
color : black;
margin : 20px;
background : #e6e8ea;
text-align : center;
}
a {
color : #b31616;
font-weight : bold;
}
a:link, a:visited, a:active {}
a:hover {
color : red;
}
h1, h2, h3, h4, h5, h6 {
font-family : Arial, sans-serif;
font-weight : normal;
}
h1 {
color : #b31616;
font-weight : bold;
letter-spacing : -2px;
font-size : 2.2em;
border-bottom : 1px solid silver;
padding-bottom : 5px;
}
h2 {
font-size : 1.5em;
border-bottom : 1px solid silver;
padding-bottom : 3px;
clear : both;
}
h3 { font-size : 1.2em; }
h4 { font-size : 1.1em; }
h5 { font-size : 1.0em; }
h6 { font-size : 0.8em; }
img { border : 0; }
ol, ul, li, p, pre, table, tr, td, th { font-size : 1.0em; }
pre { font-family : monospace; }
strong, b { font-weight : bold; }
td, th {
border : 1px solid #aaaaaa;
border-collapse : collapse;
padding : 3px;
}
th {
background : #3667a8;
color : white;
}
ol ol { list-style-type : lower-alpha; }
.content {
text-align : left;
margin-left : auto;
margin-right : auto;
width : 780px;
background-color : white;
border-left : 1px solid black;
border-right : 1px solid black;
padding : 12px 30px;
line-height : 150%;
}
.logo {
background : white url("Other/Help/Images/Help_Background_Header.png") repeat-x;
width : 840px;
margin-top : 20px;
margin-left : auto;
margin-right : auto;
text-align : left;
border-right : 1px solid black;
border-left : 1px solid black;
}
.footer {
background : white url("Other/Help/Images/Help_Background_Footer.png") repeat-x;
width : 840px;
height : 16px;
margin-left : auto;
margin-right : auto;
text-align : left;
border-right : 1px solid black;
border-left : 1px solid black;
}
.logo img {
padding-left : 0px;
border : none;
position : relative;
top : -4px;
}
* html .content { width : 760px; }
* html .logo, * html .footer { width : 820px; }
.content h1 { margin : 0px; }
h1.hastagline { border : 0; }
h2.tagline {
color : #747673;
clear : none;
margin-top : 0em;
}
/* printer styles */
@media print {
body, .content {
margin : 0;
padding : 0;
}
.navigation, .locator, .footer a, .message, .footer-links { display : none; }
.footer, .content, .header { border : none; }
a {
text-decoration : none;
font-weight : normal;
color : black;
}
}
</style>
</head>
<body>
<div class="logo"><a href="http://portableapps.com/"><img src="Other/Help/Images/Help_Logo_Top.png" alt="PortableApps.com - Your Digital Life, Anywhere"></a></div>
<div class="content">
<h1 class="hastagline">tw blue Portable Help</h1>
<h2 class="tagline">A powerful and accessible Twitter client</h2>
<p>tw blue Portable is the tw blue whatever it is packaged with a PortableApps.com launcher as a <a href="http://portableapps.com/about/what_is_a_portable_app">portable app</a>, so you can view and send tweets on your iPod, USB flash drive, portable hard drive, etc. It has all the same features as tw blue, plus, it leaves no personal information behind on the machine you run it on, so you can take it with you wherever you go. <a href="http://twblue.es">Learn more about tw blue...</a></p>
<p><a href="http://portableapps.com/donate"><img src="Other/Help/Images/Donation_Button.png" style="vertical-align:middle" alt="Make a Donation"></a> - Support PortableApps.com's Hosting and Development</p>
<p><a href="http://portableapps.com/node/*Node ID*">Go to the tw blue Portable Homepage &gt;&gt;</a></p>
<p><a href="http://portableapps.com/">Get more portable apps at PortableApps.com</a></p>
<p>This software is OSI Certified Open Source Software. OSI Certified is a certification mark of the Open Source Initiative.</p>
<h2>Portable App Issues</h2>
<ul>
<li><a href="http://portableapps.com/support/portable_app#downloading">Downloading a Portable App</a></li>
<li><a href="http://portableapps.com/support/portable_app#installing">Installing a Portable App</a></li>
<li><a href="http://portableapps.com/support/portable_app#using">Using a Portable App</a></li>
<li><a href="http://portableapps.com/support/portable_app#upgrading">Upgrading a Portable App</a></li>
</ul>
<p>You can read about advanced configuration options for the PortableApps.com Launcher in its <a href="Other/Source/Readme.txt">readme file</a>.</p>
</div>
<div class="footer"></div>
</body>
</html>

17
release-notes.md Normal file
View File

@@ -0,0 +1,17 @@
## Changelog
* 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:
* 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))
* Implemented update profile dialog. ([#547](https://github.com/MCV-Software/TWBlue/pull/547))
* 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))
* Added possibility to vote in polls. This is mapped to Alt+Win+Shift+V in the invisible keymaps for windows 10/11.
* 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.

16
scripts/build.ps1 Normal file
View File

@@ -0,0 +1,16 @@
# Build a TW Blue installer.
# Must be called from root of repo
echo "Generating documentation..."
cd doc
python documentation_importer.py
python generator.py
mv documentation ..\src
cd ..
echo "done."
echo "Building binary..."
cd src
python write_version_data.py
python setup.py build
cd ..
echo "done."

View File

@@ -1,12 +0,0 @@
import shutil
import os
import sys
def create_archive():
os.chdir("..\\src")
print("Creating zip archive...")
folder = "dist"
shutil.make_archive("twblue", "zip", folder)
os.chdir("..\\scripts")
create_archive()

View File

@@ -58,7 +58,8 @@ SetOutPath "$INSTDIR"
${If} ${RunningX64}
File /r TWBlue64\*
${Else}
File /r TWBlue\*
messagebox MB_ICONSTOP "Error: This TWBlue installer is only compatible with 64-bit systems. TWBlue does not support 32 bit systems any more."
Quit
${EndIf}
CreateShortCut "$DESKTOP\TWBlue.lnk" "$INSTDIR\TWBlue.exe"
!insertmacro MUI_STARTMENU_WRITE_BEGIN startmenu

View File

@@ -1,48 +0,0 @@
#! /usr/bin/env python
import sys
import os
import glob
import ftplib
transferred=0
def convert_bytes(n):
K, M, G, T, P = 1 << 10, 1 << 20, 1 << 30, 1 << 40, 1 << 50
if n >= P:
return '%.2fPb' % (float(n) / T)
elif n >= T:
return '%.2fTb' % (float(n) / T)
elif n >= G:
return '%.2fGb' % (float(n) / G)
elif n >= M:
return '%.2fMb' % (float(n) / M)
elif n >= K:
return '%.2fKb' % (float(n) / K)
else:
return '%d' % n
def callback(progress):
global transferred
transferred = transferred+len(progress)
print("Uploaded {}".format(convert_bytes(transferred),))
ftp_server = os.environ.get("FTP_SERVER") or sys.argv[1]
ftp_username = os.environ.get("FTP_USERNAME") or sys.argv[2]
ftp_password = os.environ.get("FTP_PASSWORD") or sys.argv[3]
print("Uploading files to the TWBlue server...")
print("Connecting to %s" % (ftp_server,))
connection = ftplib.FTP(ftp_server)
print("Connected to FTP server {}".format(ftp_server,))
connection.login(user=ftp_username, passwd=ftp_password)
print("Logged in successfully")
connection.cwd("web/pubs")
files = glob.glob("*.zip")+glob.glob("*.exe")
print("These files will be uploaded into the version folder: {}".format(files,))
for file in files:
transferred = 0
print("Uploading {}".format(file,))
with open(file, "rb") as f:
connection.storbinary('STOR %s' % file, f, callback=callback, blocksize=1024*1024)
print("Upload completed. exiting...")
connection.quit()

View File

@@ -1,14 +1,13 @@
# -*- coding: utf-8 -*-
name = 'TWBlue'
short_name='twblue'
update_url = 'https://twblue.es/updates/updates.php'
mirror_update_url = 'https://raw.githubusercontent.com/mcv-software/TWBlue/next-gen/updates/updates.json'
update_url = 'https://raw.githubusercontent.com/mcv-software/TWBlue/next-gen/updates/updates.json'
authors = ["Manuel Cortéz", "José Manuel Delicado"]
authorEmail = "manuel@manuelcortez.net"
copyright = "Copyright (C) 2013-2023, MCV Software."
copyright = "Copyright (C) 2013-2024, MCV Software."
description = name+" is an app designed to use Twitter simply and efficiently while using minimal system resources. This app provides access to most Twitter features."
translators = ["Manuel Cortéz (English)", "Mohammed Al Shara, Hatoun Felemban (Arabic)", "Francisco Torres (Catalan)", "Manuel cortéz (Spanish)", "Sukil Etxenike Arizaleta (Basque)", "Jani Kinnunen (finnish)", "Corentin Bacqué-Cazenave (Français)", "Juan Buño (Galician)", "Steffen Schultz (German)", "Zvonimir Stanečić (Croatian)", "Robert Osztolykan (Hungarian)", "Christian Leo Mameli (Italian)", "Riku (Japanese)", "Paweł Masarczyk (Polish)", "Odenilton Júnior Santos (Portuguese)", "Florian Ionașcu, Nicușor Untilă (Romanian)", "Natalia Hedlund, Valeria Kuznetsova (Russian)", "Aleksandar Đurić (Serbian)", "Burak Yüksek (Turkish)"]
url = "https://twblue.es"
url = "https://twblue.mcvsoftware.com"
report_bugs_url = "https://github.com/MCV-Software/TWBlue/issues"
supported_languages = []
version = "11"

View File

@@ -3,4 +3,5 @@ from .base import BaseBuffer
from .mentions import MentionsBuffer
from .conversations import ConversationBuffer, ConversationListBuffer
from .users import UserBuffer
from .notifications import NotificationsBuffer
from .notifications import NotificationsBuffer
from .search import SearchBuffer

View File

@@ -40,7 +40,7 @@ class BaseBuffer(base.Buffer):
self.buffer.account = account
self.bind_events()
self.sound = sound
if "-timeline" in self.name or "-followers" in self.name or "-following" in self.name:
if "-timeline" in self.name or "-followers" in self.name or "-following" in self.name or "searchterm" in self.name:
self.finished_timeline = False
def create_buffer(self, parent, name):
@@ -263,7 +263,10 @@ class BaseBuffer(base.Buffer):
menu = menus.base()
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
if self.can_share() == True:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
else:
menu.boost.Enable(False)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.fav, menuitem=menu.fav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.unfav, menuitem=menu.unfav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.url_, menuitem=menu.openUrl)
@@ -310,14 +313,16 @@ class BaseBuffer(base.Buffer):
if index > -1 and self.session.db.get(self.name) != None:
return self.session.db[self.name][index]
def can_share(self):
post = self.get_item()
if post.visibility == "direct":
def can_share(self, item=None):
if item == None:
item = self.get_item()
if item.visibility == "direct":
return False
return True
def reply(self, *args):
item = self.get_item()
def reply(self, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
visibility = item.visibility
if visibility == "direct":
title = _("Conversation with {}").format(item.account.username)
@@ -352,8 +357,9 @@ class BaseBuffer(base.Buffer):
if hasattr(post.message, "destroy"):
post.message.destroy()
def send_message(self, *args, **kwargs):
item = self.get_item()
def send_message(self, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
title = _("Conversation with {}").format(item.account.username)
caption = _("Write your message here")
if item.reblog != None:
@@ -378,11 +384,12 @@ class BaseBuffer(base.Buffer):
if hasattr(post.message, "destroy"):
post.message.destroy()
def share_item(self, *args, **kwargs):
if self.can_share() == False:
def share_item(self, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
if self.can_share(item=item) == False:
return output.speak(_("This action is not supported on conversations."))
post = self.get_item()
id = post.id
id = item.id
if self.session.settings["general"]["boost_mode"] == "ask":
answer = mastodon_dialogs.boost_question()
if answer == True:
@@ -407,12 +414,11 @@ class BaseBuffer(base.Buffer):
pub.sendMessage("toggleShare", shareable=can_share)
self.buffer.boost.Enable(can_share)
def audio(self, url='', *args, **kwargs):
def audio(self, item=None, *args, **kwargs):
if sound.URLPlayer.player.is_playing():
return sound.URLPlayer.stop_audio()
item = self.get_item()
if item == None:
return
item = self.get_item()
urls = utils.get_media_urls(item)
if len(urls) == 1:
url=urls[0]
@@ -428,25 +434,25 @@ class BaseBuffer(base.Buffer):
# except:
# log.error("Exception while executing audio method.")
def url(self, url='', announce=True, *args, **kwargs):
if url == '':
post = self.get_item()
if post.reblog != None:
urls = utils.find_urls(post.reblog)
else:
urls = utils.find_urls(post)
if len(urls) == 1:
url=urls[0]
elif len(urls) > 1:
urls_list = urlList.urlList()
urls_list.populate_list(urls)
if urls_list.get_response() == widgetUtils.OK:
url=urls_list.get_string()
if hasattr(urls_list, "destroy"): urls_list.destroy()
if url != '':
if announce:
output.speak(_(u"Opening URL..."), True)
webbrowser.open_new_tab(url)
def url(self, announce=True, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
if item.reblog != None:
urls = utils.find_urls(item.reblog)
else:
urls = utils.find_urls(item)
if len(urls) == 1:
url=urls[0]
elif len(urls) > 1:
urls_list = urlList.urlList()
urls_list.populate_list(urls)
if urls_list.get_response() == widgetUtils.OK:
url=urls_list.get_string()
if hasattr(urls_list, "destroy"): urls_list.destroy()
if url != '':
if announce:
output.speak(_(u"Opening URL..."), True)
webbrowser.open_new_tab(url)
def clear_list(self):
dlg = commonMessageDialogs.clear_list()
@@ -476,31 +482,37 @@ class BaseBuffer(base.Buffer):
item = self.get_item()
pass
def get_item_url(self):
post = self.get_item()
if post.reblog != None:
return post.reblog.url
return post.url
def get_item_url(self, item=None):
if item == None:
item = self.get_item()
if item.reblog != None:
return item.reblog.url
return item.url
def open_in_browser(self, *args, **kwargs):
url = self.get_item_url()
def open_in_browser(self, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
url = self.get_item_url(item=item)
output.speak(_("Opening item in web browser..."))
webbrowser.open(url)
def add_to_favorites(self):
item = self.get_item()
def add_to_favorites(self, item=None):
if item == None:
item = self.get_item()
if item.reblog != None:
item = item.reblog
call_threaded(self.session.api_call, call_name="status_favourite", preexec_message=_("Adding to favorites..."), _sound="favourite.ogg", id=item.id)
def remove_from_favorites(self):
item = self.get_item()
def remove_from_favorites(self, item=None):
if item == None:
item = self.get_item()
if item.reblog != None:
item = item.reblog
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, *args, **kwargs):
item = self.get_item()
def toggle_favorite(self, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
if item.reblog != None:
item = item.reblog
try:
@@ -513,8 +525,9 @@ class BaseBuffer(base.Buffer):
else:
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, *args, **kwargs):
item = self.get_item()
def toggle_bookmark(self, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
if item.reblog != None:
item = item.reblog
try:
@@ -527,16 +540,17 @@ class BaseBuffer(base.Buffer):
else:
call_threaded(self.session.api_call, call_name="status_unbookmark", preexec_message=_("Removing from bookmarks..."), _sound="favourite.ogg", id=item.id)
def view_item(self):
post = self.get_item()
def view_item(self, item=None):
if item == None:
item = self.get_item()
# Update object so we can retrieve newer stats
try:
post = self.session.api.status(id=post.id)
item = self.session.api.status(id=item.id)
except MastodonNotFoundError:
output.speak(_("No status found with that ID"))
return
# print(post)
msg = messages.viewPost(post, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url())
msg = messages.viewPost(item, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url(item=item))
def ocr_image(self):
post = self.get_item()

View File

@@ -51,7 +51,7 @@ class ConversationListBuffer(BaseBuffer):
results = getattr(self.session.api, self.function)(min_id=min_id, limit=count, *self.args, **self.kwargs)
results.reverse()
except Exception as e:
log.exception("Error %s" % (str(e)))
log.exception("Error %s loading %s with args of %r and kwargs of %r" % (str(e), self.function, self.args, self.kwargs))
return
new_position, number_of_items = self.order_buffer(results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
@@ -110,6 +110,9 @@ class ConversationListBuffer(BaseBuffer):
self.session.db[self.name] = []
objects = self.session.db[self.name]
for i in data:
# Deleted conversations handling.
if i.last_status == None:
continue
position = self.get_item_position(i)
if position != None:
conversation = self.session.db[self.name][position]

View File

@@ -3,15 +3,23 @@ import time
import logging
import widgetUtils
import output
from pubsub import pub
from controller.buffers.mastodon.base import BaseBuffer
from controller.mastodon import messages
from sessions.mastodon import compose, templates
from wxUI import buffers
from wxUI.dialogs.mastodon import dialogs as mastodon_dialogs
from wxUI.dialogs.mastodon import menus
from mysc.thread_utils import call_threaded
log = logging.getLogger("controller.buffers.mastodon.notifications")
class NotificationsBuffer(BaseBuffer):
def __init__(self, *args, **kwargs):
super(NotificationsBuffer, self).__init__(*args, **kwargs)
self.type = "notificationsBuffer"
def get_message(self):
notification = self.get_item()
if notification == None:
@@ -36,16 +44,90 @@ class NotificationsBuffer(BaseBuffer):
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)
def fav(self):
pass
def unfav(self):
pass
def vote(self):
pass
def can_share(self):
def can_share(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
return super(NotificationsBuffer, self).can_share(item=item.status)
return False
def add_to_favorites(self):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).add_to_favorites(item=item.status)
def remove_from_favorites(self):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).remove_from_favorites(item=item.status)
def toggle_favorite(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).toggle_favorite(item=item.status)
def toggle_bookmark(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).toggle_bookmark(item=item.status)
def reply(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).reply(item=item.status)
def share_item(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).share_item(item=item.status)
def url(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).url(item=item.status, *args, **kwargs)
def audio(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).audio(item=item.status)
def view_item(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).view_item(item=item.status)
else:
pub.sendMessage("execute-action", action="user_details")
def open_in_browser(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).open_in_browser(item=item.status)
def send_message(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).send_message(item=item.status)
else:
item = self.get_item()
title = _("New conversation with {}").format(item.account.username)
caption = _("Write your message here")
users_str = "@{} ".format(item.account.acct)
post = messages.post(session=self.session, title=title, caption=caption, text=users_str)
post.message.visibility.SetSelection(3)
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, visibility="direct")
if hasattr(post.message, "destroy"):
post.message.destroy()
def is_post(self):
post_types = ["status", "mention", "reblog", "favourite", "update", "poll"]
item = self.get_item()
if item.type in post_types:
return True
return False
def destroy_status(self, *args, **kwargs):
@@ -64,3 +146,29 @@ class NotificationsBuffer(BaseBuffer):
self.session.sound.play("error.ogg")
log.exception("")
self.session.db[self.name] = items
def show_menu(self, ev, pos=0, *args, **kwargs):
if self.buffer.list.get_count() == 0:
return
notification = self.get_item()
menu = menus.notification(notification.type)
if self.is_post():
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
if self.can_share() == True:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
else:
menu.boost.Enable(False)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.fav, menuitem=menu.fav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.unfav, menuitem=menu.unfav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.url_, menuitem=menu.openUrl)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.audio, menuitem=menu.play)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.view, menuitem=menu.view)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.copy, menuitem=menu.copy)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.destroy_status, menuitem=menu.remove)
if hasattr(menu, "openInBrowser"):
widgetUtils.connect_event(menu, widgetUtils.MENU, self.open_in_browser, menuitem=menu.openInBrowser)
if pos != 0:
self.buffer.PopupMenu(menu, pos)
else:
self.buffer.PopupMenu(menu, self.buffer.list.list.GetPosition())

View File

@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""
Implements searching functionality for mastodon
Used for searching for statuses (posts) or possibly hashtags
"""
import logging
import time
from pubsub import pub
from .base import BaseBuffer
import output
import widgetUtils
from wxUI import commonMessageDialogs
log = logging.getLogger("controller.buffers.mastodon.search")
class SearchBuffer(BaseBuffer):
"""Search buffer
There are some methods of the Base Buffer that can't be used here
"""
def start_stream(self, mandatory: bool=False, play_sound: bool=True, avoid_autoreading: bool=False) -> None:
"""Start streaming
Parameters:
- mandatory [bool]: Force start stream if True
- play_sound [bool]: Specifies whether to play sound after receiving posts
avoid_autoreading [bool]: Reads the posts if set to True
returns [None | int]: Number of posts received
"""
log.debug(f"Starting streamd for buffer {self.name} account {self.account} and type {self.type}")
log.debug(f"Args: {self.args}, Kwargs: {self.kwargs}")
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
min_id = None
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 = getattr(self.session.api, self.function)(min_id=min_id, **self.kwargs)
except Exception as mess:
log.exception(f"Error while receiving search posts {mess}")
return
results = results.statuses
results.reverse()
num_of_items = self.session.order_buffer(self.name, results)
log.debug(f"Number of items retrieved: {num_of_items}")
self.put_items_on_list(num_of_items)
# playsound and autoread
if num_of_items > 0:
if 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)
if avoid_autoreading == False and mandatory == True and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(num_of_items)
return num_of_items
def remove_buffer(self, force: bool=False) -> bool:
"""Performs clean-up tasks before removing buffer
Parameters:
- force [bool]: Force removes buffer if true
Returns [bool]: True proceed with removing buffer or False abort
removing buffer
"""
# Ask user
if not force:
response = commonMessageDialogs.remove_buffer()
else:
response = widgetUtils.YES
if response == widgetUtils.NO:
return False
# remove references of this buffer in db and settings
if self.name in self.session.db:
self.session.db.pop(self.name)
if self.kwargs.get('q') in self.session.settings['other_buffers']['post_searches']:
self.session.settings['other_buffers']['post_searches'].remove(self.kwargs['q'])
return True
def get_more_items(self):
output.speak(_(u"This action is not supported for this buffer"), True)

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import os
import sys
import logging
import webbrowser
import wx
@@ -116,6 +117,7 @@ class Controller(object):
# connect application events to GUI
widgetUtils.connect_event(self.view, widgetUtils.CLOSE_EVENT, self.exit_)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.show_hide, menuitem=self.view.show_hide)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.update_profile, menuitem=self.view.updateProfile)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.search, menuitem=self.view.menuitem_search)
# widgetUtils.connect_event(self.view, widgetUtils.MENU, self.list_manager, menuitem=self.view.lists)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.find, menuitem=self.view.find)
@@ -138,6 +140,7 @@ class Controller(object):
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.delete, self.view.delete)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.follow, menuitem=self.view.follow)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.send_dm, self.view.dm)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.user_details, self.view.details)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.get_more_items, menuitem=self.view.load_previous_items)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.clear_buffer, menuitem=self.view.clear)
widgetUtils.connect_event(self.view, widgetUtils.MENU, self.remove_buffer, self.view.deleteTl)
@@ -427,6 +430,10 @@ class Controller(object):
return handler.account_settings(buffer=buffer, controller=self)
def check_for_updates(self, *args, **kwargs):
if not getattr(sys, 'frozen', False):
log.debug("Running from source, can't update")
commonMessageDialogs.cant_update_source()
return
update = updater.do_update()
if update == False:
view.no_update_available()
@@ -892,7 +899,7 @@ class Controller(object):
except:
output.speak(_(u"An error happened while trying to connect to the server. Please try later."))
return
# There is no twblue.es/en, so if English is the language used this should be False anyway.
# There is no twblue.mcvsoftware.com/en, so if English is the language used this should be False anyway.
if response.status_code == 200 and lang != "en":
webbrowser.open_new_tab(final_url)
else:
@@ -908,7 +915,7 @@ class Controller(object):
except:
output.speak(_(u"An error happened while trying to connect to the server. Please try later."))
return
# There is no twblue.es/en, so if English is the language used this should be False anyway.
# There is no twblue.mcvsoftware.com/en, so if English is the language used this should be False anyway.
if response.status_code == 200 and lang != "en":
webbrowser.open_new_tab(final_url)
else:
@@ -984,9 +991,9 @@ class Controller(object):
def repeat_item(self, *args, **kwargs):
output.speak(self.get_current_buffer().get_message())
def execute_action(self, action):
def execute_action(self, action, kwargs={}):
if hasattr(self, action):
getattr(self, action)()
getattr(self, action)(**kwargs)
def update_buffers(self):
for i in self.buffers[:]:
@@ -1085,3 +1092,52 @@ class Controller(object):
"""Redirects the user to the issue page on github"""
log.debug("Redirecting the user to report an error...")
webbrowser.open_new_tab(application.report_bugs_url)
def update_profile(self, *args):
"""Updates the users profile"""
log.debug("Update profile")
buffer = self.get_best_buffer()
handler = self.get_handler(buffer.session.type)
if handler:
handler.update_profile(buffer.session)
def user_details(self, *args):
"""Displays a user's profile."""
log.debug("Showing user profile...")
buffer = self.get_best_buffer()
handler = self.get_handler(type=buffer.session.type)
if handler and hasattr(handler, 'user_details'):
handler.user_details(buffer)
def openPostTimeline(self, *args, user=None):
"""Opens selected user's posts timeline
Parameters:
args: Other argument. Useful when binding to widgets.
user: if specified, open this user timeline. It is currently mandatory, but could be optional when user selection is implemented in handler
"""
buffer = self.get_best_buffer()
handler = self.get_handler(type=buffer.session.type)
if handler and hasattr(handler, 'openPostTimeline'):
handler.openPostTimeline(self, buffer, user)
def openFollowersTimeline(self, *args, user=None):
"""Opens selected user's followers timeline
Parameters:
args: Other argument. Useful when binding to widgets.
user: if specified, open this user timeline. It is currently mandatory, but could be optional when user selection is implemented in handler
"""
buffer = self.get_best_buffer()
handler = self.get_handler(type=buffer.session.type)
if handler and hasattr(handler, 'openFollowersTimeline'):
handler.openFollowersTimeline(self, buffer, user)
def openFollowingTimeline(self, *args, user=None):
"""Opens selected user's following timeline
Parameters:
args: Other argument. Useful when binding to widgets.
user: if specified, open this user timeline. It is currently mandatory, but could be optional when user selection is implemented in handler
"""
buffer = self.get_best_buffer()
handler = self.get_handler(type=buffer.session.type)
if handler and hasattr(handler, 'openFollowingTimeline'):
handler.openFollowingTimeline(self, buffer, user)

View File

@@ -4,10 +4,14 @@ import logging
import output
from pubsub import pub
from mysc import restart
from mysc.thread_utils import call_threaded
from wxUI.dialogs.mastodon import search as search_dialogs
from wxUI.dialogs.mastodon import dialogs
from wxUI.dialogs import userAliasDialogs
from wxUI import commonMessageDialogs
from wxUI.dialogs.mastodon import updateProfile as update_profile_dialogs
from wxUI.dialogs.mastodon import showUserProfile
from sessions.mastodon.utils import html_filter
from . import userActions, settings
log = logging.getLogger("controller.mastodon.handler")
@@ -20,7 +24,7 @@ class Handler(object):
# empty names mean the item will be Disabled.
self.menus = dict(
# In application menu.
updateProfile=None,
updateProfile=_("Update Profile"),
menuitem_search=_("&Search"),
lists=None,
manageAliases=_("Manage user aliases"),
@@ -41,7 +45,7 @@ class Handler(object):
addAlias=_("Add a&lias"),
addToList=None,
removeFromList=None,
details=None,
details=_("Show user profile"),
favs=None,
# In buffer Menu.
trends=None,
@@ -98,9 +102,9 @@ class Handler(object):
# 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="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", session.db["user_name"])
# for i in session.settings["other_buffers"]["tweet_searches"]:
# pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_(u"Search for {}").format(i), parent_tab=searches_position, start=False, kwargs=dict(parent=controller.view.nb, function="search_tweets", name="%s-searchterm" % (i,), sessionObject=session, name, bufferType="searchPanel", sound="search_updated.ogg", q=i, include_ext_alt_text=True, tweet_mode="extended"))
searches_position =controller.view.search("searches", session.db["user_name"])
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"))
# 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"))
@@ -138,6 +142,17 @@ class Handler(object):
users = [user.acct for user in item.mentions if user.id != buffer.session.db["user_id"]]
if item.account.acct not in users:
users.insert(0, item.account.acct)
elif buffer.type == "notificationsBuffer":
if buffer.is_post():
status = item.status
if status.reblog != None:
users = [user.acct for user in status.reblog.mentions if user.id != buffer.session.db["user_id"]]
if status.reblog.account.acct not in users and status.account.id != buffer.session.db["user_id"]:
users.insert(0, status.reblog.account.acct)
else:
users = [user.acct for user in status.mentions if user.id != buffer.session.db["user_id"]]
if item.account.acct not in users:
users.insert(0, item.account.acct)
u = userActions.userActions(buffer.session, users)
def search(self, controller, session, value):
@@ -150,7 +165,7 @@ class Handler(object):
if term not in session.settings["other_buffers"]["post_searches"]:
session.settings["other_buffers"]["post_searches"].append(term)
session.settings.write()
# 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, function="search_tweets", name="%s-searchterm" % (term,), sessionObject=session, account=session.get_name(), bufferType="searchPanel", sound="search_updated.ogg", q=term, include_ext_alt_text=True, tweet_mode="extended"))
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"))
else:
log.error("A buffer for the %s search term is already created. You can't create a duplicate buffer." % (term,))
return
@@ -182,38 +197,52 @@ class Handler(object):
return
user = u.user
if action == "posts":
if user.statuses_count == 0:
dialogs.no_posts()
return
if user.id in buffer.session.settings["other_buffers"]["timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=buffer.session.type, buffer_title=_("Timeline for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="account_statuses", name="%s-timeline" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="tweet_timeline.ogg", id=user.id))
buffer.session.settings["other_buffers"]["timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
self.openPostTimeline(controller, buffer, user)
elif action == "followers":
if user.followers_count == 0:
dialogs.no_followers()
return
if user.id in buffer.session.settings["other_buffers"]["followers_timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=buffer.session.type, buffer_title=_("Followers for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_followers", name="%s-followers" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", id=user.id))
buffer.session.settings["other_buffers"]["followers_timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
self.openFollowersTimeline(controller, buffer, user)
elif action == "following":
if user.following_count == 0:
dialogs.no_following()
return
if user.id in buffer.session.settings["other_buffers"]["following_timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=buffer.session.type, buffer_title=_("Following for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_following", name="%s-followers" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", id=user.id))
buffer.session.settings["other_buffers"]["following_timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
self.openFollowingTimeline(controller, buffer, user)
def openPostTimeline(self, controller, buffer, user):
"""Opens post timeline for user"""
if user.statuses_count == 0:
dialogs.no_posts()
return
if user.id in buffer.session.settings["other_buffers"]["timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=buffer.session.type, buffer_title=_("Timeline for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="account_statuses", name="%s-timeline" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="tweet_timeline.ogg", id=user.id))
buffer.session.settings["other_buffers"]["timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
buffer.session.settings.write()
def openFollowersTimeline(self, controller, buffer, user):
"""Open followers timeline for user"""
if user.followers_count == 0:
dialogs.no_followers()
return
if user.id in buffer.session.settings["other_buffers"]["followers_timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=buffer.session.type, buffer_title=_("Followers for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_followers", name="%s-followers" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", id=user.id))
buffer.session.settings["other_buffers"]["followers_timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
buffer.session.settings.write()
def openFollowingTimeline(self, controller, buffer, user):
"""Open following timeline for user"""
if user.following_count == 0:
dialogs.no_following()
return
if user.id in buffer.session.settings["other_buffers"]["following_timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=buffer.session.type, buffer_title=_("Following for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_following", name="%s-followers" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", id=user.id))
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):
@@ -254,4 +283,75 @@ class Handler(object):
buffer.session.settings["user-aliases"][str(full_user.id)] = alias
buffer.session.settings.write()
output.speak(_("Alias has been set correctly for {}.").format(user))
pub.sendMessage("alias-added")
pub.sendMessage("alias-added")
def update_profile(self, session):
"""Updates the users dialog"""
profile = session.api.me()
data = {
'display_name': profile.display_name,
'note': html_filter(profile.note),
'header': profile.header,
'avatar': profile.avatar,
'fields': [(field.name, html_filter(field.value)) for field in profile.fields],
'locked': profile.locked,
'bot': profile.bot,
# discoverable could be None, set it to False
'discoverable': profile.discoverable if profile.discoverable else False,
}
log.debug(f"Received data_ {data['fields']}")
dialog = update_profile_dialogs.UpdateProfileDialog(**data)
if dialog.ShowModal() != wx.ID_OK:
log.debug("User canceled dialog")
return
updated_data = dialog.data
if updated_data == data:
log.debug("No profile info was changed.")
return
# remove data that hasn't been updated
for key in data:
if data[key] == updated_data[key]:
del updated_data[key]
log.debug(f"Updating users profile with: {updated_data}")
call_threaded(session.api_call, "account_update_credentials", _("Update profile"), report_success=True, **updated_data)
def user_details(self, buffer):
"""Displays user profile in a dialog.
This works as long as the focused item hass a 'account' key."""
if not hasattr(buffer, 'get_item'):
return # Tell user?
item = buffer.get_item()
if not item:
return # empty buffer
log.debug(f"Opening user profile. dictionary: {item}")
mentionedUsers = list()
holdUser = item.account if item.get('account') else None
if hasattr(item, "type") and item.type in ["status", "mention", "reblog", "favourite", "update", "poll"]: # statuses in Notification buffers
item = item.status
if item.get('username'): # account dict
holdUser = item
elif isinstance(item.get('mentions'), list):
# mentions in statuses
if item.reblog:
item = item.reblog
mentionedUsers = [(user.acct, user.id) for user in item.mentions]
holdUser = item.account
if not holdUser:
dialogs.no_user()
return
if len(mentionedUsers) == 0:
user = holdUser
else:
mentionedUsers.insert(0, (holdUser.display_name, holdUser.username, holdUser.id))
mentionedUsers = list(set(mentionedUsers))
selectedUser = showUserProfile.selectUserDialog(mentionedUsers)
if not selectedUser:
return # Canceled selection
elif selectedUser[-1] == holdUser.id:
user = holdUser
else: # We don't have this user's dictionary, get it!
user = buffer.session.api.account(selectedUser[-1])
dlg = showUserProfile.ShowUserProfile(user)
dlg.ShowModal()

View File

@@ -9,6 +9,7 @@ from twitter_text import parse_tweet, config
from controller import messages
from sessions.mastodon import templates
from wxUI.dialogs.mastodon import postDialogs
from extra.autocompletionUsers import completion
def character_count(post_text, post_cw, character_limit=500):
# We will use text for counting character limit only.
@@ -38,14 +39,17 @@ class post(messages.basicMessage):
widgetUtils.connect_event(self.message.translate, widgetUtils.BUTTON_PRESSED, self.translate)
widgetUtils.connect_event(self.message.add, widgetUtils.BUTTON_PRESSED, self.on_attach)
widgetUtils.connect_event(self.message.remove_attachment, widgetUtils.BUTTON_PRESSED, self.remove_attachment)
# ToDo: Add autocomplete feature to mastodon and uncomment this.
# widgetUtils.connect_event(self.message.autocomplete_users, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
widgetUtils.connect_event(self.message.autocomplete_users, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
widgetUtils.connect_event(self.message.add_post, widgetUtils.BUTTON_PRESSED, self.add_post)
widgetUtils.connect_event(self.message.remove_post, widgetUtils.BUTTON_PRESSED, self.remove_post)
self.attachments = []
self.thread = []
self.text_processor()
def autocomplete_users(self, *args, **kwargs):
c = completion.autocompletionUsers(self.message, self.session.session_id)
c.show_menu()
def add_post(self, event, update_gui=True, *args, **kwargs):
text = self.message.text.GetValue()
attachments = self.attachments[::]

View File

@@ -9,7 +9,8 @@ import output
from collections import OrderedDict
from wxUI import commonMessageDialogs
from wxUI.dialogs.mastodon import configuration
#from extra.autocompletionUsers import scan, manage
from extra.autocompletionUsers import manage
from extra.autocompletionUsers.mastodon import scan
from extra.ocr import OCRSpace
from controller.settings import globalSettingsController
from . templateEditor import EditTemplate
@@ -29,8 +30,8 @@ class accountSettingsController(globalSettingsController):
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)
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", "disable_streaming", self.config["general"]["disable_streaming"])
self.dialog.set_value("general", "relative_time", self.config["general"]["relative_times"])
self.dialog.set_value("general", "read_preferences_from_instance", self.config["general"]["read_preferences_from_instance"])

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
""" Autocompletion users for TWBlue. This package contains all needed code to support this feature, including automatic addition of users, management and code to show the autocompletion menu when an user is composing a post. """

View File

@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
""" Module to display the user autocompletion menu in post dialogs. """
import output
from . import storage
from . import wx_menu
class autocompletionUsers(object):
def __init__(self, window, session_id):
""" Class constructor. Displays a menu with users matching the specified pattern for autocompletion.
:param window: A wx control where the menu should be displayed. Normally this is going to be the wx.TextCtrl indicating the tweet's text or direct message recipient.
:type window: wx.Dialog
:param session_id: Session ID which calls this class. We will load the users database from this session.
:type session_id: str.
"""
super(autocompletionUsers, self).__init__()
self.window = window
self.db = storage.storage(session_id)
def show_menu(self, mode="mastodon"):
""" displays a menu with possible users matching the specified pattern.
this menu can be displayed in dialogs where an username is expected. For Mastodon's post dialogs, the string should start with an at symbol (@), otherwise it won't match the pattern.
Of course, users must be already loaded in database before attempting this.
If no users are found, an error message will be spoken.
:param mode: this controls how the dialog will behave. Possible values are 'mastodon' and 'free'. In mastodon mode, the matching pattern will be @user (@ is required), while in 'free' mode the matching pattern will be anything written in the text control.
:type mode: str
"""
if mode == "mastodon":
position = self.window.text.GetInsertionPoint()
text = self.window.text.GetValue()
text = text[:position]
try:
pattern = text.split()[-1]
except IndexError:
output.speak(_(u"You have to start writing"))
return
if pattern.startswith("@") == True:
menu = wx_menu.menu(self.window.text, pattern[1:], mode=mode)
users = self.db.get_users(pattern[1:])
if len(users) > 0:
menu.append_options(users)
self.window.PopupMenu(menu, self.window.text.GetPosition())
menu.destroy()
else:
output.speak(_(u"There are no results in your users database"))
else:
output.speak(_(u"Autocompletion only works for users."))
elif mode == "free":
text = self.window.cb.GetValue()
try:
pattern = text.split()[-1]
except IndexError:
output.speak(_(u"You have to start writing"))
return
menu = wx_menu.menu(self.window.cb, pattern, mode=mode)
users = self.db.get_users(pattern)
if len(users) > 0:
menu.append_options(users)
self.window.PopupMenu(menu, self.window.cb.GetPosition())
menu.destroy()
else:
output.speak(_(u"There are no results in your users database"))

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
""" Management of users in the local database for autocompletion. """
import time
import widgetUtils
from wxUI import commonMessageDialogs
from . import storage, wx_manage
from .mastodon import scan as mastodon
class autocompletionManage(object):
def __init__(self, session):
""" class constructor. Manages everything related to user autocompletion.
:param session: Sessiom where the autocompletion management has been requested.
:type session: sessions.base.Session.
"""
super(autocompletionManage, self).__init__()
self.session = session
# Instantiate database so we can perform modifications on it.
self.database = storage.storage(self.session.session_id)
def show_settings(self):
""" display user management dialog and connect events associated to it. """
self.dialog = wx_manage.autocompletionManageDialog()
self.users = self.database.get_all_users()
self.dialog.put_users(self.users)
widgetUtils.connect_event(self.dialog.add, widgetUtils.BUTTON_PRESSED, self.add_user)
widgetUtils.connect_event(self.dialog.remove, widgetUtils.BUTTON_PRESSED, self.remove_user)
self.dialog.get_response()
def update_list(self):
""" update users list in management dialog. This function is normallhy used after we modify the database in any way, so we can reload all users in the autocompletion user management list. """
item = self.dialog.users.get_selected()
self.dialog.users.clear()
self.users = self.database.get_all_users()
self.dialog.put_users(self.users)
self.dialog.users.select_item(item)
def add_user(self, *args, **kwargs):
""" Add a new username to the autocompletion database. """
usr = self.dialog.get_user()
if usr == False:
return
user_added = False
if self.session.type == "mastodon":
user_added = mastodon.add_user(session=self.session, database=self.database, user=usr)
if user_added == False:
self.dialog.show_invalid_user_error()
return
self.update_list()
def remove_user(self, *args, **kwargs):
""" Remove focused user from the autocompletion database. """
if commonMessageDialogs.delete_user_from_db() == widgetUtils.YES:
item = self.dialog.users.get_selected()
user = self.users[item]
self.database.remove_user(user[0])
self.update_list()

View File

@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
""" Scanning code for autocompletion feature on TWBlue. This module can retrieve user objects from the selected Mastodon account automatically. """
import time
import wx
import widgetUtils
import output
from pubsub import pub
from . import wx_scan
from extra.autocompletionUsers import manage, storage
class autocompletionScan(object):
def __init__(self, config, buffer, window):
""" Class constructor. This class will take care of scanning the selected Mastodon account to populate the database with users automatically upon request.
:param config: Config for the session that will be scanned in search for users.
:type config: dict
:param buffer: home buffer for the focused session.
:type buffer: controller.buffers.mastodon.base.baseBuffer
:param window: Main Window of TWBlue.
:type window:wx.Frame
"""
super(autocompletionScan, self).__init__()
self.config = config
self.buffer = buffer
self.window = window
def show_dialog(self):
""" displays a dialog to confirm which buffers should be scanned (followers or following users). """
self.dialog = wx_scan.autocompletionScanDialog()
self.dialog.set("friends", self.config["mysc"]["save_friends_in_autocompletion_db"])
self.dialog.set("followers", self.config["mysc"]["save_followers_in_autocompletion_db"])
if self.dialog.get_response() == widgetUtils.OK:
confirmation = wx_scan.confirm()
return confirmation
def prepare_progress_dialog(self):
self.progress_dialog = wx_scan.autocompletionScanProgressDialog()
# connect method to update progress dialog
pub.subscribe(self.on_update_progress, "on-update-progress")
self.progress_dialog.Show()
def on_update_progress(self):
wx.CallAfter(self.progress_dialog.progress_bar.Pulse)
def scan(self):
""" Attempts to add all users selected by current user to the autocomplete database. """
self.config["mysc"]["save_friends_in_autocompletion_db"] = self.dialog.get("friends")
self.config["mysc"]["save_followers_in_autocompletion_db"] = self.dialog.get("followers")
output.speak(_("Updating database... You can close this window now. A message will tell you when the process finishes."))
database = storage.storage(self.buffer.session.session_id)
percent = 0
users = []
if self.dialog.get("friends") == True:
first_page = self.buffer.session.api.account_following(id=self.buffer.session.db["user_id"], limit=80)
pub.sendMessage("on-update-progress")
if first_page != None:
for user in first_page:
users.append(user)
next_page = first_page
while next_page != None:
next_page = self.buffer.session.api.fetch_next(next_page)
pub.sendMessage("on-update-progress")
if next_page == None:
break
for user in next_page:
users.append(user)
# same step, but for followers.
if self.dialog.get("followers") == True:
first_page = self.buffer.session.api.account_followers(id=self.buffer.session.db["user_id"], limit=80)
pub.sendMessage("on-update-progress")
if first_page != None:
for user in first_page:
if user not in users:
users.append(user)
next_page = first_page
while next_page != None:
next_page = self.buffer.session.api.fetch_next(next_page)
pub.sendMessage("on-update-progress")
if next_page == None:
break
for user in next_page:
if user not in users:
users.append(user)
# except TweepyException:
# wx.CallAfter(wx_scan.show_error)
# return self.done()
for user in users:
name = user.display_name if user.display_name != None and user.display_name != "" else user.username
database.set_user(user.acct, name, 1)
wx.CallAfter(wx_scan .show_success, len(users))
self.done()
def done(self):
wx.CallAfter(self.progress_dialog.Destroy)
wx.CallAfter(self.dialog.Destroy)
pub.unsubscribe(self.on_update_progress, "on-update-progress")
def add_user(session, database, user):
""" Adds an user to the database. """
user = session.api.account_lookup(user)
if user != None:
name = user.display_name if user.display_name != None and user.display_name != "" else user.username
database.set_user(user.acct, name, 1)

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
import wx
import widgetUtils
import application
class autocompletionScanDialog(widgetUtils.BaseDialog):
def __init__(self):
super(autocompletionScanDialog, self).__init__(parent=None, id=-1, title=_(u"Autocomplete users' settings"))
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
self.followers = wx.CheckBox(panel, -1, _("Add followers to database"))
self.friends = wx.CheckBox(panel, -1, _("Add following to database"))
sizer.Add(self.followers, 0, wx.ALL, 5)
sizer.Add(self.friends, 0, wx.ALL, 5)
ok = wx.Button(panel, wx.ID_OK)
cancel = wx.Button(panel, wx.ID_CANCEL)
sizerBtn = wx.BoxSizer(wx.HORIZONTAL)
sizerBtn.Add(ok, 0, wx.ALL, 5)
sizer.Add(cancel, 0, wx.ALL, 5)
sizer.Add(sizerBtn, 0, wx.ALL, 5)
panel.SetSizer(sizer)
self.SetClientSize(sizer.CalcMin())
class autocompletionScanProgressDialog(widgetUtils.BaseDialog):
def __init__(self, *args, **kwargs):
super(autocompletionScanProgressDialog, self).__init__(parent=None, id=wx.ID_ANY, title=_("Updating autocompletion database"), *args, **kwargs)
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
self.progress_bar = wx.Gauge(parent=panel)
sizer.Add(self.progress_bar)
panel.SetSizerAndFit(sizer)
def confirm():
with wx.MessageDialog(None, _("This process will retrieve the users you selected from your Mastodon account, and add them to the user autocomplete database. Please note that if there are many users or you have tried to perform this action less than 15 minutes ago, TWBlue may reach a limit in API calls when trying to load the users into the database. If this happens, we will show you an error, in which case you will have to try this process again in a few minutes. If this process ends with no error, you will be redirected back to the account settings dialog. Do you want to continue?"), _("Attention"), style=wx.ICON_QUESTION|wx.YES_NO) as result:
if result.ShowModal() == wx.ID_YES:
return True
return False
def show_success(users):
with wx.MessageDialog(None, _("TWBlue has imported {} users successfully.").format(users), _("Done")) as dlg:
dlg.ShowModal()
def show_error():
with wx.MessageDialog(None, _("Error adding users from Mastodon. Please try again in about 15 minutes."), _("Error"), style=wx.ICON_ERROR) as dlg:
dlg.ShowModal()

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
import os, sqlite3, paths
class storage(object):
def __init__(self, session_id):
self.connection = sqlite3.connect(os.path.join(paths.config_path(), "%s/autocompletionUsers.dat" % (session_id)))
self.cursor = self.connection.cursor()
if self.table_exist("users") == False:
self.create_table()
def table_exist(self, table):
ask = self.cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='%s'" % (table))
answer = ask.fetchone()
if answer == None:
return False
else:
return True
def get_all_users(self):
self.cursor.execute("""select * from users""")
return self.cursor.fetchall()
def get_users(self, term):
self.cursor.execute("""SELECT * FROM users WHERE UPPER(user) LIKE :term OR UPPER(name) LIKE :term""", {"term": "%{}%".format(term.upper())})
return self.cursor.fetchall()
def set_user(self, screen_name, user_name, from_a_buffer):
self.cursor.execute("""insert or ignore into users values(?, ?, ?)""", (screen_name, user_name, from_a_buffer))
self.connection.commit()
def remove_user(self, user):
self.cursor.execute("""DELETE FROM users WHERE user = ?""", (user,))
self.connection.commit()
return self.cursor.fetchone()
def remove_by_buffer(self, bufferType):
""" Removes all users saved on a buffer. BufferType is 0 for no buffer, 1 for friends and 2 for followers"""
self.cursor.execute("""DELETE FROM users WHERE from_a_buffer = ?""", (bufferType,))
self.connection.commit()
return self.cursor.fetchone()
def create_table(self):
self.cursor.execute("""
create table users(
user TEXT UNIQUE,
name TEXT,
from_a_buffer INTEGER
)""")
def __del__(self):
self.cursor.close()
self.connection.close()

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
import wx
import widgetUtils
from multiplatform_widgets import widgets
import application
class autocompletionManageDialog(widgetUtils.BaseDialog):
def __init__(self):
super(autocompletionManageDialog, self).__init__(parent=None, id=-1, title=_(u"Manage Autocompletion database"))
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
label = wx.StaticText(panel, -1, _(u"Editing {0} users database").format(application.name,))
self.users = widgets.list(panel, _(u"Username"), _(u"Name"), style=wx.LC_REPORT)
sizer.Add(label, 0, wx.ALL, 5)
sizer.Add(self.users.list, 0, wx.ALL, 5)
self.add = wx.Button(panel, -1, _(u"Add user"))
self.remove = wx.Button(panel, -1, _(u"Remove user"))
optionsBox = wx.BoxSizer(wx.HORIZONTAL)
optionsBox.Add(self.add, 0, wx.ALL, 5)
optionsBox.Add(self.remove, 0, wx.ALL, 5)
sizer.Add(optionsBox, 0, wx.ALL, 5)
ok = wx.Button(panel, wx.ID_OK)
cancel = wx.Button(panel, wx.ID_CANCEL)
sizerBtn = wx.BoxSizer(wx.HORIZONTAL)
sizerBtn.Add(ok, 0, wx.ALL, 5)
sizer.Add(cancel, 0, wx.ALL, 5)
sizer.Add(sizerBtn, 0, wx.ALL, 5)
panel.SetSizer(sizer)
self.SetClientSize(sizer.CalcMin())
def put_users(self, users):
for i in users:
j = [i[0], i[1]]
self.users.insert_item(False, *j)
def get_user(self):
usr = False
userDlg = wx.TextEntryDialog(None, _(u"Twitter username"), _(u"Add user to database"))
if userDlg.ShowModal() == wx.ID_OK:
usr = userDlg.GetValue()
return usr
def show_invalid_user_error(self):
wx.MessageDialog(None, _(u"The user does not exist"), _(u"Error!"), wx.ICON_ERROR).ShowModal()

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
import wx
class menu(wx.Menu):
def __init__(self, window, pattern, mode):
super(menu, self).__init__()
self.window = window
self.pattern = pattern
self.mode = mode
def append_options(self, options):
for i in options:
item = wx.MenuItem(self, wx.ID_ANY, "%s (@%s)" % (i[1], i[0]))
self.Append(item)
self.Bind(wx.EVT_MENU, lambda evt, temp=i[0]: self.select_text(evt, temp), item)
def select_text(self, ev, text):
if self.mode == "mastodon":
self.window.ChangeValue(self.window.GetValue().replace("@"+self.pattern, "@"+text+" "))
elif self.mode == "free":
self.window.SetValue(self.window.GetValue().replace(self.pattern, text))
self.window.SetInsertionPointEnd()
def destroy(self):
self.Destroy()

View File

@@ -35,4 +35,5 @@ accountConfiguration = string(default="control+win+shift+o")
update_buffer = string(default="control+win+shift+u")
ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
add_alias=string(default="")
vote=string(default="alt+win+shift+v")

View File

@@ -54,4 +54,5 @@ accountConfiguration = string(default="control+win+shift+o")
update_buffer = string(default="control+win+shift+u")
ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
add_alias=string(default="")
vote=string(default="alt+win+shift+v")

View File

@@ -57,3 +57,4 @@ ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
find = string(default="control+win+{")
vote=string(default="alt+win+shift+v")

View File

@@ -57,3 +57,4 @@ ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
find = string(default="control+win+{")
vote=string(default="alt+win+shift+v")

View File

@@ -57,4 +57,5 @@ accountConfiguration = string(default="control+win+shift+o")
update_buffer = string(default="control+win+shift+u")
ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
add_alias=string(default="")
vote=string(default="alt+win+shift+v")

View File

@@ -15,7 +15,7 @@ actions = {
"remove_from_favourites": _(u"Remove post from favorites"),
"toggle_like": _("Add/remove post from favorites"),
"follow": _(u"Open the user actions dialogue"),
# "user_details": _(u"See user details"),
"user_details": _(u"See user details"),
"view_item": _(u"Show post"),
"exit": _(u"Quit"),
"open_timeline": _(u"Open user timeline"),

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

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

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

View File

@@ -104,7 +104,7 @@ def proxy_setup():
def donation():
dlg = commonMessageDialogs.donation()
if dlg == widgetUtils.YES:
webbrowser.open_new_tab(_("https://twblue.es/donate"))
webbrowser.open_new_tab(_("https://twblue.mcvsoftware.com/donate"))
config.app["app-settings"]["donation_dialog_displayed"] = True
def check_pid():

View File

@@ -35,6 +35,7 @@ following_timelines = list(default=list())
trending_topic_buffers = list(default=list())
muted_buffers = list(default=list())
autoread_buffers = list(default=list(mentions, direct_messages, events))
post_searches = list(default = list())
[mysc]
spelling_language = string(default="")
@@ -54,4 +55,4 @@ notification = string(default="$display_name $text, $date")
[filters]
[user-aliases]
[user-aliases]

View File

@@ -135,6 +135,10 @@ class Session(base.baseSession):
else:
last_id = self.db[name][-1].id
for i in data:
# handle empty notifications.
post_types = ["status", "mention", "reblog", "favourite", "update", "poll"]
if hasattr(i, "type") and i.type in post_types and i.status == None:
continue
if ignore_older and last_id != None:
if i.id < last_id:
log.error("Ignoring an older tweet... Last id: {0}, tweet id: {1}".format(last_id, i.id))

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", "mastodon.defaults", "keymaps", "locales", "sounds", "documentation", ("keys/lib", "keys/lib"), find_sound_lib_datafiles(), find_accessible_output2_datafiles()]+get_architecture_files(),
include_files=["icon.ico", "app-configuration.defaults", "mastodon.defaults", "keymaps", "locales", "sounds", "documentation", 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

@@ -8,13 +8,13 @@ from .wxUpdater import *
logger = logging.getLogger("updater")
def do_update(endpoint=application.update_url):
if not getattr(sys, 'frozen', False):
logger.debug("Running from source, aborting update check")
return False
try:
result = update.perform_update(endpoint=endpoint, current_version=application.version, app_name=application.name, update_available_callback=available_update_dialog, progress_callback=progress_callback, update_complete_callback=update_finished)
return result
except:
if endpoint == application.update_url:
logger.error("Update failed! Using mirror URL...")
return do_update(endpoint=application.mirror_update_url)
else:
logger.exception("Update failed.")
output.speak("An exception occurred while attempting to update " + application.name + ". If this message persists, contact the " + application.name + " developers. More information about the exception has been written to the error log.",True)
return result
logger.exception("Update failed.")
output.speak("An exception occurred while attempting to update " + application.name + ". If this message persists, contact the " + application.name + " developers. More information about the exception has been written to the error log.",True)
return None

View File

@@ -2,17 +2,13 @@
""" Write version info (taken from the last commit) to application.py. This method has been implemented this way for running updates.
This file is not intended to be called by the user. It will be used only by the Gitlab CI runner."""
import os
import requests
from codecs import open
print("Writing version data for update...")
commit_info = requests.get("https://gitlab.com/api/v4/projects/23482196/repository/commits/next-gen")
commit_info = commit_info.json()
commit = commit_info["short_id"]
print("Got new version info: {commit}".format(commit=commit,))
new_version = os.environ.get("GITHUB_REF_NAME")[1:]
file = open("application.py", "r", encoding="utf-8")
lines = file.readlines()
lines[-1] = 'version = "{}"'.format(commit_info["created_at"][:10].replace("-", "."))
lines[-1] = 'version = "{}"'.format(new_version)
file.close()
file2 = open("application.py", "w", encoding="utf-8")
file2.writelines(lines)
@@ -22,7 +18,7 @@ print("Wrote application.py with the new version info.")
print("Updating next version on installer setup...")
file = open("..\\scripts\\twblue.nsi", "r", encoding="utf-8")
contents = file.read()
contents = contents.replace("0.95", commit_info["created_at"][:10].replace("-", "."))
contents = contents.replace("0.95", new_version)
file.close()
file2 = open("..\\scripts\\twblue.nsi", "w", encoding="utf-8")
file2.write(contents)

View File

@@ -38,3 +38,9 @@ def invalid_configuration():
def dead_pid():
return wx.MessageDialog(None, _(u"{0} quit unexpectedly the last time it was run. If the problem persists, please report it to the {0} developers.").format(application.name), _(u"Warning"), wx.OK).ShowModal()
def cant_update_source() -> wx.MessageDialog:
"""Shows a dialog telling a user he /she can't update because he / she is
running from source
"""
dlg = wx.MessageDialog(None, _("Sorry, you can't update while running {} from source.").format(application.name), _("Error"), wx.OK)
return dlg.ShowModal()

View File

@@ -13,9 +13,7 @@ class generalAccount(wx.Panel, baseDialog.BaseWXDialog):
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)

View File

@@ -52,4 +52,11 @@ def no_followers():
def no_following():
dlg = wx.MessageDialog(None, _("This user is not following anyone. {0} can't create a timeline.").format(application.name), _(u"Error"), wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
dlg.Destroy()
dlg.Destroy()
def no_user():
dlg = wx.MessageDialog(None, _("The focused item has no user in it. {} ca't open a user profile").format(application.name), _(u"Error"), wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()

View File

@@ -14,15 +14,43 @@ class base(wx.Menu):
self.Append(self.unfav)
self.openUrl = wx.MenuItem(self, wx.ID_ANY, _("&Open URL"))
self.Append(self.openUrl)
self.openInBrowser = wx.MenuItem(self, wx.ID_ANY, _(u"&Open in Twitter"))
self.openInBrowser = wx.MenuItem(self, wx.ID_ANY, _(u"&Open in instance"))
self.Append(self.openInBrowser)
self.play = wx.MenuItem(self, wx.ID_ANY, _(u"&Play audio"))
self.Append(self.play)
self.view = wx.MenuItem(self, wx.ID_ANY, _(u"&Show tweet"))
self.view = wx.MenuItem(self, wx.ID_ANY, _(u"&Show post"))
self.Append(self.view)
self.copy = wx.MenuItem(self, wx.ID_ANY, _(u"&Copy to clipboard"))
self.Append(self.copy)
self.remove = wx.MenuItem(self, wx.ID_ANY, _(u"&Delete"))
self.Append(self.remove)
self.userActions = wx.MenuItem(self, wx.ID_ANY, _(u"&User actions..."))
self.Append(self.userActions)
self.Append(self.userActions)
class notification(wx.Menu):
def __init__(self, item="status"):
super(notification, self).__init__()
valid_types = ["status", "mention", "reblog", "favourite", "update", "poll"]
if item in valid_types:
self.boost = wx.MenuItem(self, wx.ID_ANY, _("&Boost"))
self.Append(self.boost)
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
self.Append(self.reply)
self.fav = wx.MenuItem(self, wx.ID_ANY, _(u"&Add to favorites"))
self.Append(self.fav)
self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites"))
self.Append(self.unfav)
self.openUrl = wx.MenuItem(self, wx.ID_ANY, _("&Open URL"))
self.Append(self.openUrl)
self.play = wx.MenuItem(self, wx.ID_ANY, _(u"&Play audio"))
self.Append(self.play)
self.openInBrowser = wx.MenuItem(self, wx.ID_ANY, _(u"&Open in instance"))
self.Append(self.openInBrowser)
self.view = wx.MenuItem(self, wx.ID_ANY, _(u"&Show post"))
self.Append(self.view)
self.copy = wx.MenuItem(self, wx.ID_ANY, _(u"&Copy to clipboard"))
self.Append(self.copy)
self.remove = wx.MenuItem(self, wx.ID_ANY, _(u"&Dismiss"))
self.Append(self.remove)
self.userActions = wx.MenuItem(self, wx.ID_ANY, _(u"&User actions..."))
self.Append(self.userActions)

View File

@@ -0,0 +1,239 @@
# -*- coding: utf-8 -*-
"""Wx dialogs for showing a user's profile."""
from io import BytesIO
from pubsub import pub
from typing import Tuple
import requests
import wx
from logging import getLogger
from threading import Thread
from sessions.mastodon.utils import html_filter
log = getLogger(__name__)
def selectUserDialog(users: list) -> tuple:
"""Choose a user from a possible list of users"""
if len(users) == 1:
return users[0]
dlg = wx.Dialog(None, title=_("Select user"))
label = wx.StaticText(dlg, label="Select a user: ")
choiceList = []
for user in users:
if len(user) == 3: # (display_name, username, id)
choiceList.append(f"{user[0]}: @{user[1]}")
else: # (acct, id)
choiceList.append(f"{user[0]}")
choice = wx.Choice(dlg, choices=choiceList)
ok = wx.Button(dlg, wx.ID_OK, _("OK"))
ok.SetDefault()
cancel = wx.Button(dlg, wx.ID_CANCEL, _("Cancel"))
dlg.SetEscapeId(cancel.GetId())
#sizers
sizer = wx.GridSizer(2, 2, 5, 5)
sizer.Add(label, wx.SizerFlags().Center())
sizer.Add(choice, wx.SizerFlags().Center())
sizer.Add(ok, wx.SizerFlags().Center())
sizer.Add(cancel, wx.SizerFlags().Center())
if dlg.ShowModal() == wx.ID_CANCEL:
return ()
# return the selected user
return users[choice.GetSelection()]
def returnTrue():
return True
class ShowUserProfile(wx.Dialog):
"""
A dialog for Showing user profile
layout is:
```
header
avatar
name
bio
meta data
```
"""
def __init__(self, user):
"""Initialize update profile dialog
Parameters:
- user: user dictionary
"""
super().__init__(parent=None)
self.user = user
self.SetTitle(_("{}'s Profile").format(user.display_name))
self.panel = wx.Panel(self)
wrapperSizer = wx.BoxSizer(wx.VERTICAL)
mainSizer = wx.GridSizer(2, 5, 5)
# create widgets
nameLabel = wx.StaticText(self.panel, label=_("Name: "))
name = self.createTextCtrl(user.display_name, size=(200, 30))
mainSizer.Add(nameLabel, wx.SizerFlags().Center())
mainSizer.Add(name, wx.SizerFlags().Center())
urlLabel = wx.StaticText(self.panel, label=_("URL: "))
url = self.createTextCtrl(user.url, size=(200, 30))
mainSizer.Add(urlLabel, wx.SizerFlags().Center())
mainSizer.Add(url, wx.SizerFlags().Center())
bioLabel = wx.StaticText(self.panel, label=_("Bio: "))
bio = self.createTextCtrl(html_filter(user.note), (400, 60))
mainSizer.Add(bioLabel, wx.SizerFlags().Center())
mainSizer.Add(bio, wx.SizerFlags().Center())
joinLabel = wx.StaticText(self.panel, label=_("Joined at: "))
joinText = self.createTextCtrl(user.created_at.strftime('%d %B, %Y'), (80, 30))
mainSizer.Add(joinLabel, wx.SizerFlags().Center())
mainSizer.Add(joinText, wx.SizerFlags().Center())
actions = wx.Button(self.panel, label=_("Actions"))
actions.Bind(wx.EVT_BUTTON, self.onAction)
mainSizer.Add(actions, wx.SizerFlags().Center())
# header
headerLabel = wx.StaticText(self.panel, label=_("Header: "))
# Create empty image
self.headerImage = wx.StaticBitmap(self.panel)
self.headerImage.AcceptsFocusFromKeyboard = returnTrue
mainSizer.Add(headerLabel, wx.SizerFlags().Center())
mainSizer.Add(self.headerImage, wx.SizerFlags().Center())
# avatar
avatarLabel = wx.StaticText(self.panel, label=_("Avatar: "))
# Create empty image
self.avatarImage = wx.StaticBitmap(self.panel)
self.avatarImage.AcceptsFocusFromKeyboard = returnTrue
mainSizer.Add(avatarLabel, wx.SizerFlags().Center())
mainSizer.Add(self.avatarImage, wx.SizerFlags().Center())
self.fields = []
for num, field in enumerate(user.fields):
labelSizer = wx.BoxSizer(wx.HORIZONTAL)
labelLabel = wx.StaticText(self.panel, label=_("Field {} - Label: ").format(num + 1))
labelSizer.Add(labelLabel, wx.SizerFlags().Center().Border(wx.ALL, 5))
labelText = self.createTextCtrl(html_filter(field.name), (230, 30), True)
labelSizer.Add(labelText, wx.SizerFlags().Expand().Border(wx.ALL, 5))
mainSizer.Add(labelSizer, 0, wx.CENTER)
contentSizer = wx.BoxSizer(wx.HORIZONTAL)
contentLabel = wx.StaticText(self.panel, label=_("Content: "))
contentSizer.Add(contentLabel, wx.SizerFlags().Center())
contentText = self.createTextCtrl(html_filter(field.value), (400, 60), True)
contentSizer.Add(contentText, wx.SizerFlags().Center())
mainSizer.Add(contentSizer, 0, wx.CENTER | wx.LEFT, 10)
bullSwitch = {True: _('Yes'), False: _('No'), None: _('No')}
privateSizer = wx.BoxSizer(wx.HORIZONTAL)
privateLabel = wx.StaticText(self.panel, label=_("Private account: "))
private = self.createTextCtrl(bullSwitch[user.locked], (30, 30))
privateSizer.Add(privateLabel, wx.SizerFlags().Center())
privateSizer.Add(private, wx.SizerFlags().Center())
mainSizer.Add(privateSizer, 0, wx.ALL | wx.CENTER)
botSizer = wx.BoxSizer(wx.HORIZONTAL)
botLabel = wx.StaticText(self.panel, label=_("Bot account: "))
botText = self.createTextCtrl(bullSwitch[user.bot], (30, 30))
botSizer.Add(botLabel, wx.SizerFlags().Center())
botSizer.Add(botText, wx.SizerFlags().Center())
mainSizer.Add(botSizer, 0, wx.ALL | wx.CENTER)
discoverSizer = wx.BoxSizer(wx.HORIZONTAL)
discoverLabel = wx.StaticText(self.panel, label=_("Discoverable account: "))
discoverText = self.createTextCtrl(bullSwitch[user.discoverable], (30, 30))
discoverSizer.Add(discoverLabel, wx.SizerFlags().Center())
discoverSizer.Add(discoverText, wx.SizerFlags().Center())
mainSizer.Add(discoverSizer, 0, wx.ALL | wx.CENTER)
posts = wx.Button(self.panel, label=_("{} posts. Click to open posts timeline").format(user.statuses_count))
# posts.SetToolTip(_("Click to open {}'s posts").format(user.display_name))
posts.Bind(wx.EVT_BUTTON, self.onPost)
mainSizer.Add(posts, wx.SizerFlags().Center())
following = wx.Button(self.panel, label=_("{} following. Click to open Following timeline").format(user.following_count))
mainSizer.Add(following, wx.SizerFlags().Center())
following.Bind(wx.EVT_BUTTON, self.onFollowing)
followers = wx.Button(self.panel, label=_("{} followers. Click to open followers timeline").format(user.followers_count))
mainSizer.Add(followers, wx.SizerFlags().Center())
followers.Bind(wx.EVT_BUTTON, self.onFollowers)
close = wx.Button(self.panel, wx.ID_CLOSE, _("Close"))
self.SetEscapeId(close.GetId())
close.SetDefault()
wrapperSizer.Add(mainSizer, 0, wx.CENTER)
wrapperSizer.Add(close, wx.SizerFlags().Center())
self.panel.SetSizer(wrapperSizer)
wrapperSizer.Fit(self.panel)
self.panel.Center()
mainSizer.Fit(self)
self.Center()
imageDownloaderThread = Thread(target=self._getImages)
imageDownloaderThread.start()
def createTextCtrl(self, text: str, size: Tuple[int, int], multiline: bool = False) -> wx.TextCtrl:
"""Creates a wx.TextCtrl and returns it
Parameters:
text: The value of the wx.TextCtrl
size: The size of the wx.TextCtrl
Returns: the created wx.TextCtrl object
"""
if not multiline:
style= wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_READONLY
else:
style= wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_READONLY | wx.TE_MULTILINE
textCtrl = wx.TextCtrl(self.panel, value=text, size=size, style=style)
textCtrl.AcceptsFocusFromKeyboard = returnTrue
return textCtrl
def onAction(self, *args):
"""Opens the Open timeline dialog"""
pub.sendMessage('execute-action', action='follow')
def onPost(self, *args):
"""Open this user's timeline"""
pub.sendMessage('execute-action', action='openPostTimeline', kwargs=dict(user=self.user))
def onFollowing(self, *args):
"""Open following timeline for this user"""
pub.sendMessage('execute-action', action='openFollowingTimeline', kwargs=dict(user=self.user))
def onFollowers(self, *args):
"""Open followers timeline for this user"""
pub.sendMessage('execute-action', action='openFollowersTimeline', kwargs=dict(user=self.user))
def _getImages(self):
"""Downloads image from mastodon server
This method should run on a separate thread
"""
log.debug("Downloading user's header and avatar images...")
try:
header = requests.get(self.user.header)
avatar = requests.get(self.user.avatar)
except requests.exceptions.RequestException as mess:
log.exception("An exception was raised while downloading images:", mess)
return
wx.CallAfter(self._drawImages, header.content, avatar.content)
def _drawImages(self, headerImageBytes, avatarImageBytes):
"""Draws images on the bitmap ui"""
# log.debug("Drawing images...")
# Header
headerImage = wx.Image(BytesIO(headerImageBytes), wx.BITMAP_TYPE_ANY)
headerImage.Rescale(300, 100, wx.IMAGE_QUALITY_HIGH)
self.headerImage.SetBitmap(headerImage.ConvertToBitmap())
# Avatar
avatarImage = wx.Image(BytesIO(avatarImageBytes), wx.BITMAP_TYPE_ANY)
avatarImage.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
self.avatarImage.SetBitmap(avatarImage.ConvertToBitmap())

View File

@@ -0,0 +1,204 @@
import os
import requests
from io import BytesIO
import wx
def return_true():
return True
class UpdateProfileDialog(wx.Dialog):
"""
A dialog for user to update his / her profile details.
layout is:
```
header
avatar
name
bio
meta data
```
"""
def __init__(self, display_name: str, note: str, header: str, avatar: str, fields: list, locked: bool, bot: bool, discoverable: bool):
"""Initialize update profile dialog
Parameters:
- display_name: The user's display name to show in the display name field
- note: The users bio to show in the bio field
- header: the users header pic link
- avatar: The users avatar pic link
"""
super().__init__(parent=None)
self.SetTitle(_("Update Profile"))
self.header = header
self.avatar = avatar
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
# create widgets
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))
name_sizer = wx.BoxSizer(wx.HORIZONTAL)
name_sizer.Add(display_name_label, wx.SizerFlags().Center())
name_sizer.Add(self.display_name, wx.SizerFlags().Center())
sizer.Add(name_sizer, wx.CENTER)
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))
bio_sizer = wx.BoxSizer(wx.HORIZONTAL)
bio_sizer.Add(bio_label, wx.SizerFlags().Center())
bio_sizer.Add(self.bio, wx.SizerFlags().Center())
sizer.Add(bio_sizer, wx.CENTER)
# header
header_label = wx.StaticText(panel, label=_("Header"))
try:
response = requests.get(self.header)
except requests.exceptions.RequestException:
# Create empty image
self.header_image = wx.StaticBitmap()
else:
image_bytes = BytesIO(response.content)
image = wx.Image(image_bytes, wx.BITMAP_TYPE_ANY)
image.Rescale(300, 100, wx.IMAGE_QUALITY_HIGH)
self.header_image = wx.StaticBitmap(panel, bitmap=image.ConvertToBitmap())
self.header_image.AcceptsFocusFromKeyboard = return_true
self.change_header = wx.Button(panel, label=_("Change header"))
header_sizer = wx.BoxSizer(wx.HORIZONTAL)
header_sizer.Add(header_label, wx.SizerFlags().Center())
header_sizer.Add(self.header_image, wx.SizerFlags().Center())
header_sizer.Add(self.change_header, wx.SizerFlags().Center())
sizer.Add(header_sizer, wx.CENTER)
# avatar
avatar_label = wx.StaticText(panel, label=_("Avatar"))
try:
response = requests.get(self.avatar)
except requests.exceptions.RequestException:
# Create empty image
self.avatar_image = wx.StaticBitmap()
else:
image_bytes = BytesIO(response.content)
image = wx.Image(image_bytes, wx.BITMAP_TYPE_ANY)
image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
self.avatar_image = wx.StaticBitmap(panel, bitmap=image.ConvertToBitmap())
self.avatar_image.AcceptsFocusFromKeyboard = return_true
self.change_avatar = wx.Button(panel, label=_("Change avatar"))
avatar_sizer = wx.BoxSizer(wx.HORIZONTAL)
avatar_sizer.Add(avatar_label, wx.SizerFlags().Center())
avatar_sizer.Add(self.avatar_image, wx.SizerFlags().Center())
avatar_sizer.Add(self.change_avatar, wx.SizerFlags().Center())
sizer.Add(avatar_sizer, wx.CENTER)
self.fields = []
for i in range(1, 5):
field_sizer = wx.BoxSizer(wx.HORIZONTAL)
field_label = wx.StaticText(panel, label=_("Field {}: Label").format(i))
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))
if i <= len(fields):
label_textctrl.SetValue(fields[i-1][0])
field_sizer.Add(label_textctrl, wx.SizerFlags().Expand().Border(wx.ALL, 5))
content_label = wx.StaticText(panel, label=_("Content"))
field_sizer.Add(content_label, wx.SizerFlags().Center().Border(wx.ALL, 5))
content_textctrl = wx.TextCtrl(panel, style=wx.TE_PROCESS_ENTER | wx.TE_MULTILINE, size=(400, 60))
if i <= len(fields):
content_textctrl.SetValue(fields[i-1][1])
field_sizer.Add(content_textctrl, wx.SizerFlags().Expand().Border(wx.ALL, 5))
sizer.Add(field_sizer, 0, wx.CENTER)
self.fields.append((label_textctrl, content_textctrl))
self.locked = wx.CheckBox(panel, label=_("Private account"))
self.locked.SetValue(locked)
self.bot = wx.CheckBox(panel, label=_("Bot account"))
self.bot.SetValue(bot)
self.discoverable = wx.CheckBox(panel, label=_("Discoverable account"))
self.discoverable.SetValue(discoverable)
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.discoverable, wx.SizerFlags().Expand().Border(wx.ALL, 5))
ok = wx.Button(panel, wx.ID_OK, _(u"&OK"))
ok.SetDefault()
cancel = wx.Button(panel, wx.ID_CANCEL, _("&Close"))
self.SetEscapeId(cancel.GetId())
action_sizer = wx.BoxSizer(wx.HORIZONTAL)
action_sizer.Add(ok, wx.SizerFlags().Center())
action_sizer.Add(cancel, wx.SizerFlags().Center())
sizer.Add(action_sizer, wx.CENTER)
panel.SetSizer(sizer)
sizer.Fit(self)
self.Center()
# manage events
ok.Bind(wx.EVT_BUTTON, self.on_ok)
self.change_header.Bind(wx.EVT_BUTTON, self.on_change_header)
self.change_avatar.Bind(wx.EVT_BUTTON, self.on_change_avatar)
self.AutoLayout = True
def on_ok(self, *args):
"""Method called when user clicks ok in dialog"""
self.data = {
'display_name': self.display_name.GetValue(),
'note': self.bio.GetValue(),
'header': self.header,
'avatar': self.avatar,
'fields': [(label.GetValue(), content.GetValue()) for label, content in self.fields if label.GetValue() and content.GetValue()],
'locked': self.locked.GetValue(),
'bot': self.bot.GetValue(),
'discoverable': self.discoverable.GetValue(),
}
self.EndModal(wx.ID_OK)
def on_change_header(self, *args):
"""Display a dialog for the user to choose a picture and update the
appropriate attribute"""
wildcard = "Images (*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif"
dlg = wx.FileDialog(self, _("Select header image - max 2MB"), wildcard=wildcard)
if dlg.ShowModal() == wx.CLOSE:
return
if os.path.getsize(dlg.GetPath()) > 2097152:
# File size exceeds the limit
message = _("The selected file is larger than 2MB. Do you want to select another file?")
caption = _("File more than 2MB")
style = wx.YES_NO | wx.ICON_WARNING
# Display the message box
result = wx.MessageBox(message, caption, style)
return self.on_change_header() if result == wx.YES else None
self.header = dlg.GetPath()
image = wx.Image(self.header, wx.BITMAP_TYPE_ANY)
image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
self.header_image.SetBitmap(image.ConvertToBitmap())
def on_change_avatar(self, *args):
"""Display a dialog for the user to choose a picture and update the
appropriate attribute"""
wildcard = "Images (*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif"
dlg = wx.FileDialog(self, _("Select avatar image - max 2MB"), wildcard=wildcard)
if dlg.ShowModal() == wx.CLOSE:
return
if os.path.getsize(dlg.GetPath()) > 2097152:
# File size exceeds the limit
message = _("The selected file is larger than 2MB. Do you want to select another file?")
caption = _("File more than 2MB")
style = wx.YES_NO | wx.ICON_WARNING
# Display the message box
result = wx.MessageBox(message, caption, style)
return self.on_change_avatar() if result == wx.YES else None
self.avatar = dlg.GetPath()
image = wx.Image(self.avatar, wx.BITMAP_TYPE_ANY)
image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
self.avatar_image.SetBitmap(image.ConvertToBitmap())

View File

@@ -15,7 +15,6 @@ class mainFrame(wx.Frame):
self.menubar_application = wx.Menu()
self.manage_accounts = self.menubar_application.Append(wx.ID_ANY, _(u"&Manage accounts"))
self.updateProfile = self.menubar_application.Append(wx.ID_ANY, _("&Update profile"))
self.updateProfile.Enable(False)
self.show_hide = self.menubar_application.Append(wx.ID_ANY, _(u"&Hide window"))
self.menuitem_search = self.menubar_application.Append(wx.ID_ANY, _(u"&Search"))
self.lists = self.menubar_application.Append(wx.ID_ANY, _(u"&Lists manager"))

View File

@@ -1,4 +1,4 @@
{"current_version": "2023.2.3",
{"current_version": "2023.04.13",
"description": "Stops support for Twitter session on Feb 9, updates to mastodon sessions. Please avoid using autoupdate as it will not work.",
"date": "unknown",
"downloads":