next_gen branch README
|
@ -16,4 +16,4 @@ include toxygen/styles/*.qss
|
||||||
include toxygen/translations/*.qm
|
include toxygen/translations/*.qm
|
||||||
include toxygen/libs/libtox.dll
|
include toxygen/libs/libtox.dll
|
||||||
include toxygen/libs/libsodium.a
|
include toxygen/libs/libsodium.a
|
||||||
include toxygen/nodes.json
|
include toxygen/bootstrap/nodes.json
|
||||||
|
|
21
README.md
|
@ -2,12 +2,6 @@
|
||||||
|
|
||||||
Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pure Python3.
|
Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pure Python3.
|
||||||
|
|
||||||
[![Release](https://img.shields.io/github/release/toxygen-project/toxygen.svg?style=flat)](https://github.com/toxygen-project/toxygen/releases/latest)
|
|
||||||
[![Stars](https://img.shields.io/github/stars/toxygen-project/toxygen.svg?style=flat)](https://github.com/toxygen-project/toxygen/stargazers)
|
|
||||||
[![Open issues](https://img.shields.io/github/issues/toxygen-project/toxygen.svg?style=flat)](https://github.com/toxygen-project/toxygen/issues)
|
|
||||||
[![License](https://img.shields.io/badge/license-GPLv3-blue.svg?style=flat)](https://raw.githubusercontent.com/toxygen-project/toxygen/master/LICENSE.md)
|
|
||||||
[![Build Status](https://travis-ci.org/toxygen-project/toxygen.svg?branch=master)](https://travis-ci.org/toxygen-project/toxygen)
|
|
||||||
|
|
||||||
### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md) - [Compile](/docs/compile.md) - [Contact](/docs/contact.md) - [Updater](https://github.com/toxygen-project/toxygen_updater)
|
### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md) - [Compile](/docs/compile.md) - [Contact](/docs/contact.md) - [Updater](https://github.com/toxygen-project/toxygen_updater)
|
||||||
|
|
||||||
### Supported OS: Linux and Windows
|
### Supported OS: Linux and Windows
|
||||||
|
@ -44,21 +38,12 @@ Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pu
|
||||||
- File resuming
|
- File resuming
|
||||||
- Read receipts
|
- Read receipts
|
||||||
|
|
||||||
### Downloads
|
|
||||||
[Releases](https://github.com/toxygen-project/toxygen/releases)
|
|
||||||
|
|
||||||
[Download last stable version](https://github.com/toxygen-project/toxygen/archive/master.zip)
|
|
||||||
|
|
||||||
[Download develop version](https://github.com/toxygen-project/toxygen/archive/develop.zip)
|
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
*Toxygen on Ubuntu and Windows*
|
*Toxygen on Ubuntu and Windows*
|
||||||
![Ubuntu](/docs/ubuntu.png)
|
![Ubuntu](/docs/ubuntu.png)
|
||||||
![Windows](/docs/windows.png)
|
![Windows](/docs/windows.png)
|
||||||
|
|
||||||
### Docs
|
## Forked
|
||||||
[Check /docs/ for more info](/docs/)
|
|
||||||
|
|
||||||
Also visit [pythonhosted.org/Toxygen/](http://pythonhosted.org/Toxygen/)
|
This hard-forked from https://github.com/toxygen-project/toxygen
|
||||||
|
```next_gen``` branch.
|
||||||
[Wiki](https://wiki.tox.chat/clients/toxygen)
|
|
||||||
|
|
|
@ -2,10 +2,18 @@
|
||||||
|
|
||||||
You can compile Toxygen using [PyInstaller](http://www.pyinstaller.org/)
|
You can compile Toxygen using [PyInstaller](http://www.pyinstaller.org/)
|
||||||
|
|
||||||
Install PyInstaller:
|
Use Dockerfile and build script from `build` directory:
|
||||||
``pip3 install pyinstaller``
|
|
||||||
|
|
||||||
Compile Toxygen:
|
1. Build image:
|
||||||
``pyinstaller --windowed --icon images/icon.ico main.py``
|
```
|
||||||
|
docker build -t toxygen .
|
||||||
|
```
|
||||||
|
|
||||||
Don't forget to copy /images/, /sounds/, /translations/, /styles/, /smileys/, /stickers/, /plugins/ (and /libs/libtox.dll, /libs/libsodium.a on Windows) to /dist/main/
|
2. Run container:
|
||||||
|
```
|
||||||
|
docker run -it toxygen bash
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Execute `build.sh` script:
|
||||||
|
|
||||||
|
```./build.sh```
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
1) Using GitHub - open issue
|
1) Using GitHub - open issue
|
||||||
|
|
||||||
2) Use Toxygen Tox Group - add bot kalina@toxme.io (or 12EDB939AA529641CE53830B518D6EB30241868EE0E5023C46A372363CAEC91C2C948AEFE4EB)
|
2) Use Toxygen Tox Group (NGC) - ID: 59D68B2709E81A679CF91416CB0E3692851C6CFCABEFF98B7131E3805A6D75FA
|
||||||
|
|
27
setup.py
|
@ -2,15 +2,17 @@ from setuptools import setup
|
||||||
from setuptools.command.install import install
|
from setuptools.command.install import install
|
||||||
from platform import system
|
from platform import system
|
||||||
from subprocess import call
|
from subprocess import call
|
||||||
from toxygen.util import program_version
|
import main
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
from utils.util import curr_directory, join_path
|
||||||
|
|
||||||
|
|
||||||
version = program_version + '.0'
|
version = main.__version__ + '.0'
|
||||||
|
|
||||||
|
|
||||||
if system() == 'Windows':
|
if system() == 'Windows':
|
||||||
MODULES = ['PyQt5', 'PyAudio', 'numpy', 'opencv-python']
|
MODULES = ['PyQt5', 'PyAudio', 'numpy', 'opencv-python', 'pydenticon']
|
||||||
else:
|
else:
|
||||||
MODULES = []
|
MODULES = []
|
||||||
try:
|
try:
|
||||||
|
@ -29,6 +31,19 @@ else:
|
||||||
import cv2
|
import cv2
|
||||||
except ImportError:
|
except ImportError:
|
||||||
MODULES.append('opencv-python')
|
MODULES.append('opencv-python')
|
||||||
|
try:
|
||||||
|
import pydenticon
|
||||||
|
except ImportError:
|
||||||
|
MODULES.append('pydenticon')
|
||||||
|
|
||||||
|
|
||||||
|
def get_packages():
|
||||||
|
directory = join_path(curr_directory(__file__), 'toxygen')
|
||||||
|
for root, dirs, files in os.walk(directory):
|
||||||
|
packages = map(lambda d: 'toxygen.' + d, dirs)
|
||||||
|
packages = ['toxygen'] + list(packages)
|
||||||
|
|
||||||
|
return packages
|
||||||
|
|
||||||
|
|
||||||
class InstallScript(install):
|
class InstallScript(install):
|
||||||
|
@ -62,7 +77,7 @@ setup(name='Toxygen',
|
||||||
author='Ingvar',
|
author='Ingvar',
|
||||||
maintainer='Ingvar',
|
maintainer='Ingvar',
|
||||||
license='GPL3',
|
license='GPL3',
|
||||||
packages=['toxygen', 'toxygen.plugins', 'toxygen.styles'],
|
packages=get_packages(),
|
||||||
install_requires=MODULES,
|
install_requires=MODULES,
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
@ -71,8 +86,8 @@ setup(name='Toxygen',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': ['toxygen=toxygen.main:main'],
|
'console_scripts': ['toxygen=toxygen.main:main']
|
||||||
},
|
},
|
||||||
cmdclass={
|
cmdclass={
|
||||||
'install': InstallScript,
|
'install': InstallScript
|
||||||
})
|
})
|
||||||
|
|
158
tests/tests.py
|
@ -1,162 +1,18 @@
|
||||||
from toxygen.profile import *
|
from toxygen.middleware.tox_factory import *
|
||||||
from toxygen.tox_dns import tox_dns
|
|
||||||
from toxygen.history import History
|
|
||||||
from toxygen.smileys import SmileyLoader
|
|
||||||
from toxygen.messages import *
|
|
||||||
import toxygen.toxes as encr
|
|
||||||
import toxygen.util as util
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: add new tests
|
||||||
|
|
||||||
class TestTox:
|
class TestTox:
|
||||||
|
|
||||||
def test_creation(self):
|
def test_creation(self):
|
||||||
name = b'Toxygen User'
|
name = 'Toxygen User'
|
||||||
status_message = b'Toxing on Toxygen'
|
status_message = 'Toxing on Toxygen'
|
||||||
tox = tox_factory()
|
tox = tox_factory()
|
||||||
tox.self_set_name(name)
|
tox.self_set_name(name)
|
||||||
tox.self_set_status_message(status_message)
|
tox.self_set_status_message(status_message)
|
||||||
data = tox.get_savedata()
|
data = tox.get_savedata()
|
||||||
del tox
|
del tox
|
||||||
tox = tox_factory(data)
|
tox = tox_factory(data)
|
||||||
assert tox.self_get_name() == str(name, 'utf-8')
|
assert tox.self_get_name() == name
|
||||||
assert tox.self_get_status_message() == str(status_message, 'utf-8')
|
assert tox.self_get_status_message() == status_message
|
||||||
|
|
||||||
|
|
||||||
class TestProfileHelper:
|
|
||||||
|
|
||||||
def test_creation(self):
|
|
||||||
file_name, path = 'test.tox', os.path.dirname(os.path.realpath(__file__)) + '/'
|
|
||||||
data = b'test'
|
|
||||||
with open(path + file_name, 'wb') as fl:
|
|
||||||
fl.write(data)
|
|
||||||
ph = ProfileHelper(path, file_name[:4])
|
|
||||||
assert ProfileHelper.get_path() == path
|
|
||||||
assert ph.open_profile() == data
|
|
||||||
assert os.path.exists(path + 'avatars/')
|
|
||||||
|
|
||||||
|
|
||||||
class TestEncryption:
|
|
||||||
|
|
||||||
def test_encr_decr(self):
|
|
||||||
tox = tox_factory()
|
|
||||||
data = tox.get_savedata()
|
|
||||||
lib = encr.ToxES()
|
|
||||||
for password in ('easypassword', 'njvnFjfn7vaGGV6', 'toxygen'):
|
|
||||||
lib.set_password(password)
|
|
||||||
copy_data = data[:]
|
|
||||||
new_data = lib.pass_encrypt(data)
|
|
||||||
assert lib.is_data_encrypted(new_data)
|
|
||||||
new_data = lib.pass_decrypt(new_data)
|
|
||||||
assert copy_data == new_data
|
|
||||||
|
|
||||||
|
|
||||||
class TestSmileys:
|
|
||||||
|
|
||||||
def test_loading(self):
|
|
||||||
settings = {'smiley_pack': 'default', 'smileys': True}
|
|
||||||
sm = SmileyLoader(settings)
|
|
||||||
assert sm.get_smileys_path() is not None
|
|
||||||
l = sm.get_packs_list()
|
|
||||||
assert len(l) == 4
|
|
||||||
|
|
||||||
|
|
||||||
def create_singletons():
|
|
||||||
folder = util.curr_directory() + '/abc'
|
|
||||||
Settings._instance = Settings.get_default_settings()
|
|
||||||
if not os.path.exists(folder):
|
|
||||||
os.makedirs(folder)
|
|
||||||
ProfileHelper(folder, 'test')
|
|
||||||
|
|
||||||
|
|
||||||
def create_friend(name, status_message, number, tox_id):
|
|
||||||
friend = Friend(None, number, name, status_message, None, tox_id)
|
|
||||||
return friend
|
|
||||||
|
|
||||||
|
|
||||||
def create_random_friend():
|
|
||||||
name, status_message, number = 'Friend', 'I am friend!', 0
|
|
||||||
tox_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6'
|
|
||||||
friend = create_friend(name, status_message, number, tox_id)
|
|
||||||
return friend
|
|
||||||
|
|
||||||
|
|
||||||
class TestFriend:
|
|
||||||
|
|
||||||
def test_friend_creation(self):
|
|
||||||
create_singletons()
|
|
||||||
name, status_message, number = 'Friend', 'I am friend!', 0
|
|
||||||
tox_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6'
|
|
||||||
friend = create_friend(name, status_message, number, tox_id)
|
|
||||||
assert friend.name == name
|
|
||||||
assert friend.tox_id == tox_id
|
|
||||||
assert friend.status_message == status_message
|
|
||||||
assert friend.number == number
|
|
||||||
|
|
||||||
def test_friend_corr(self):
|
|
||||||
create_singletons()
|
|
||||||
friend = create_random_friend()
|
|
||||||
t = time.time()
|
|
||||||
friend.append_message(InfoMessage('Info message', t))
|
|
||||||
friend.append_message(TextMessage('Hello! It is test!', MESSAGE_OWNER['ME'], t + 0.001, 0))
|
|
||||||
friend.append_message(TextMessage('Hello!', MESSAGE_OWNER['FRIEND'], t + 0.002, 0))
|
|
||||||
assert friend.get_last_message_text() == 'Hello! It is test!'
|
|
||||||
assert len(friend.get_corr()) == 3
|
|
||||||
assert len(friend.get_corr_for_saving()) == 2
|
|
||||||
friend.append_message(TextMessage('Not sent', MESSAGE_OWNER['NOT_SENT'], t + 0.002, 0))
|
|
||||||
arr = friend.get_unsent_messages_for_saving()
|
|
||||||
assert len(arr) == 1
|
|
||||||
assert arr[0][0] == 'Not sent'
|
|
||||||
tm = TransferMessage(MESSAGE_OWNER['FRIEND'],
|
|
||||||
time.time(),
|
|
||||||
TOX_FILE_TRANSFER_STATE['RUNNING'],
|
|
||||||
100, 'file_name', friend.number, 0)
|
|
||||||
friend.append_message(tm)
|
|
||||||
friend.clear_corr()
|
|
||||||
assert len(friend.get_corr()) == 1
|
|
||||||
assert len(friend.get_corr_for_saving()) == 0
|
|
||||||
friend.append_message(TextMessage('Hello! It is test!', MESSAGE_OWNER['ME'], t, 0))
|
|
||||||
assert len(friend.get_corr()) == 2
|
|
||||||
assert len(friend.get_corr_for_saving()) == 1
|
|
||||||
|
|
||||||
def test_history_search(self):
|
|
||||||
create_singletons()
|
|
||||||
friend = create_random_friend()
|
|
||||||
message = 'Hello! It is test!'
|
|
||||||
friend.append_message(TextMessage(message, MESSAGE_OWNER['ME'], time.time(), 0))
|
|
||||||
last_message = friend.get_last_message_text()
|
|
||||||
assert last_message == message
|
|
||||||
result = friend.search_string('e[m|s]')
|
|
||||||
assert result is not None
|
|
||||||
result = friend.search_string('tox')
|
|
||||||
assert result is None
|
|
||||||
|
|
||||||
|
|
||||||
class TestHistory:
|
|
||||||
|
|
||||||
def test_history(self):
|
|
||||||
create_singletons()
|
|
||||||
db_name = 'my_name'
|
|
||||||
name, status_message, number = 'Friend', 'I am friend!', 0
|
|
||||||
tox_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6'
|
|
||||||
friend = create_friend(name, status_message, number, tox_id)
|
|
||||||
history = History(db_name)
|
|
||||||
history.add_friend_to_db(friend.tox_id)
|
|
||||||
assert history.friend_exists_in_db(friend.tox_id)
|
|
||||||
text_message = 'Test!'
|
|
||||||
t = time.time()
|
|
||||||
friend.append_message(TextMessage(text_message, MESSAGE_OWNER['ME'], t, 0))
|
|
||||||
messages = friend.get_corr_for_saving()
|
|
||||||
history.save_messages_to_db(friend.tox_id, messages)
|
|
||||||
getter = history.messages_getter(friend.tox_id)
|
|
||||||
messages = getter.get_all()
|
|
||||||
assert len(messages) == 1
|
|
||||||
assert messages[0][0] == text_message
|
|
||||||
assert messages[0][1] == MESSAGE_OWNER['ME']
|
|
||||||
assert messages[0][-1] == 0
|
|
||||||
history.delete_message(friend.tox_id, t)
|
|
||||||
getter = history.messages_getter(friend.tox_id)
|
|
||||||
messages = getter.get_all()
|
|
||||||
assert len(messages) == 0
|
|
||||||
history.delete_friend_from_db(friend.tox_id)
|
|
||||||
assert not history.friend_exists_in_db(friend.tox_id)
|
|
||||||
|
|
494
toxygen/main.py
|
@ -1,485 +1,49 @@
|
||||||
import sys
|
import app
|
||||||
from loginscreen import LoginScreen
|
from user_data.settings import *
|
||||||
import profile
|
import utils.util as util
|
||||||
from settings import *
|
import argparse
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
||||||
from bootstrap import generate_nodes, download_nodes_list
|
|
||||||
from mainscreen import MainWindow
|
|
||||||
from callbacks import init_callbacks, stop, start
|
|
||||||
from util import curr_directory, program_version, remove
|
|
||||||
import styles.style # reqired for styles loading
|
|
||||||
import platform
|
|
||||||
import toxes
|
|
||||||
from passwordscreen import PasswordScreen, UnlockAppScreen, SetProfilePasswordScreen
|
|
||||||
from plugin_support import PluginLoader
|
|
||||||
import updater
|
|
||||||
|
|
||||||
|
|
||||||
class Toxygen:
|
__maintainer__ = 'Ingvar'
|
||||||
|
__version__ = '0.5.0'
|
||||||
def __init__(self, path_or_uri=None):
|
|
||||||
super(Toxygen, self).__init__()
|
|
||||||
self.tox = self.ms = self.init = self.app = self.tray = self.mainloop = self.avloop = None
|
|
||||||
if path_or_uri is None:
|
|
||||||
self.uri = self.path = None
|
|
||||||
elif path_or_uri.startswith('tox:'):
|
|
||||||
self.path = None
|
|
||||||
self.uri = path_or_uri[4:]
|
|
||||||
else:
|
|
||||||
self.path = path_or_uri
|
|
||||||
self.uri = None
|
|
||||||
|
|
||||||
def enter_pass(self, data):
|
|
||||||
"""
|
|
||||||
Show password screen
|
|
||||||
"""
|
|
||||||
tmp = [data]
|
|
||||||
p = PasswordScreen(toxes.ToxES.get_instance(), tmp)
|
|
||||||
p.show()
|
|
||||||
self.app.lastWindowClosed.connect(self.app.quit)
|
|
||||||
self.app.exec_()
|
|
||||||
if tmp[0] == data:
|
|
||||||
raise SystemExit()
|
|
||||||
else:
|
|
||||||
return tmp[0]
|
|
||||||
|
|
||||||
def main(self):
|
|
||||||
"""
|
|
||||||
Main function of app. loads login screen if needed and starts main screen
|
|
||||||
"""
|
|
||||||
app = QtWidgets.QApplication(sys.argv)
|
|
||||||
app.setWindowIcon(QtGui.QIcon(curr_directory() + '/images/icon.png'))
|
|
||||||
self.app = app
|
|
||||||
|
|
||||||
if platform.system() == 'Linux':
|
|
||||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
|
|
||||||
|
|
||||||
with open(curr_directory() + '/styles/dark_style.qss') as fl:
|
|
||||||
style = fl.read()
|
|
||||||
app.setStyleSheet(style)
|
|
||||||
|
|
||||||
encrypt_save = toxes.ToxES()
|
|
||||||
|
|
||||||
if self.path is not None:
|
|
||||||
path = os.path.dirname(self.path) + '/'
|
|
||||||
name = os.path.basename(self.path)[:-4]
|
|
||||||
data = ProfileHelper(path, name).open_profile()
|
|
||||||
if encrypt_save.is_data_encrypted(data):
|
|
||||||
data = self.enter_pass(data)
|
|
||||||
settings = Settings(name)
|
|
||||||
self.tox = profile.tox_factory(data, settings)
|
|
||||||
else:
|
|
||||||
auto_profile = Settings.get_auto_profile()
|
|
||||||
if not auto_profile[0]:
|
|
||||||
# show login screen if default profile not found
|
|
||||||
current_locale = QtCore.QLocale()
|
|
||||||
curr_lang = current_locale.languageToString(current_locale.language())
|
|
||||||
langs = Settings.supported_languages()
|
|
||||||
if curr_lang in langs:
|
|
||||||
lang_path = langs[curr_lang]
|
|
||||||
translator = QtCore.QTranslator()
|
|
||||||
translator.load(curr_directory() + '/translations/' + lang_path)
|
|
||||||
app.installTranslator(translator)
|
|
||||||
app.translator = translator
|
|
||||||
ls = LoginScreen()
|
|
||||||
ls.setWindowIconText("Toxygen")
|
|
||||||
profiles = ProfileHelper.find_profiles()
|
|
||||||
ls.update_select(map(lambda x: x[1], profiles))
|
|
||||||
_login = self.Login(profiles)
|
|
||||||
ls.update_on_close(_login.login_screen_close)
|
|
||||||
ls.show()
|
|
||||||
app.exec_()
|
|
||||||
if not _login.t:
|
|
||||||
return
|
|
||||||
elif _login.t == 1: # create new profile
|
|
||||||
_login.name = _login.name.strip()
|
|
||||||
name = _login.name if _login.name else 'toxygen_user'
|
|
||||||
pr = map(lambda x: x[1], ProfileHelper.find_profiles())
|
|
||||||
if name in list(pr):
|
|
||||||
msgBox = QtWidgets.QMessageBox()
|
|
||||||
msgBox.setWindowTitle(
|
|
||||||
QtWidgets.QApplication.translate("MainWindow", "Error"))
|
|
||||||
text = (QtWidgets.QApplication.translate("MainWindow",
|
|
||||||
'Profile with this name already exists'))
|
|
||||||
msgBox.setText(text)
|
|
||||||
msgBox.exec_()
|
|
||||||
return
|
|
||||||
self.tox = profile.tox_factory()
|
|
||||||
self.tox.self_set_name(bytes(_login.name, 'utf-8') if _login.name else b'Toxygen User')
|
|
||||||
self.tox.self_set_status_message(b'Toxing on Toxygen')
|
|
||||||
reply = QtWidgets.QMessageBox.question(None,
|
|
||||||
'Profile {}'.format(name),
|
|
||||||
QtWidgets.QApplication.translate("login",
|
|
||||||
'Do you want to set profile password?'),
|
|
||||||
QtWidgets.QMessageBox.Yes,
|
|
||||||
QtWidgets.QMessageBox.No)
|
|
||||||
if reply == QtWidgets.QMessageBox.Yes:
|
|
||||||
set_pass = SetProfilePasswordScreen(encrypt_save)
|
|
||||||
set_pass.show()
|
|
||||||
self.app.lastWindowClosed.connect(self.app.quit)
|
|
||||||
self.app.exec_()
|
|
||||||
reply = QtWidgets.QMessageBox.question(None,
|
|
||||||
'Profile {}'.format(name),
|
|
||||||
QtWidgets.QApplication.translate("login",
|
|
||||||
'Do you want to save profile in default folder? If no, profile will be saved in program folder'),
|
|
||||||
QtWidgets.QMessageBox.Yes,
|
|
||||||
QtWidgets.QMessageBox.No)
|
|
||||||
if reply == QtWidgets.QMessageBox.Yes:
|
|
||||||
path = Settings.get_default_path()
|
|
||||||
else:
|
|
||||||
path = curr_directory() + '/'
|
|
||||||
try:
|
|
||||||
ProfileHelper(path, name).save_profile(self.tox.get_savedata())
|
|
||||||
except Exception as ex:
|
|
||||||
print(str(ex))
|
|
||||||
log('Profile creation exception: ' + str(ex))
|
|
||||||
msgBox = QtWidgets.QMessageBox()
|
|
||||||
msgBox.setText(QtWidgets.QApplication.translate("login",
|
|
||||||
'Profile saving error! Does Toxygen have permission to write to this directory?'))
|
|
||||||
msgBox.exec_()
|
|
||||||
return
|
|
||||||
path = Settings.get_default_path()
|
|
||||||
settings = Settings(name)
|
|
||||||
if curr_lang in langs:
|
|
||||||
settings['language'] = curr_lang
|
|
||||||
settings.save()
|
|
||||||
else: # load existing profile
|
|
||||||
path, name = _login.get_data()
|
|
||||||
if _login.default:
|
|
||||||
Settings.set_auto_profile(path, name)
|
|
||||||
data = ProfileHelper(path, name).open_profile()
|
|
||||||
if encrypt_save.is_data_encrypted(data):
|
|
||||||
data = self.enter_pass(data)
|
|
||||||
settings = Settings(name)
|
|
||||||
self.tox = profile.tox_factory(data, settings)
|
|
||||||
else:
|
|
||||||
path, name = auto_profile
|
|
||||||
data = ProfileHelper(path, name).open_profile()
|
|
||||||
if encrypt_save.is_data_encrypted(data):
|
|
||||||
data = self.enter_pass(data)
|
|
||||||
settings = Settings(name)
|
|
||||||
self.tox = profile.tox_factory(data, settings)
|
|
||||||
|
|
||||||
if Settings.is_active_profile(path, name): # profile is in use
|
|
||||||
reply = QtWidgets.QMessageBox.question(None,
|
|
||||||
'Profile {}'.format(name),
|
|
||||||
QtWidgets.QApplication.translate("login", 'Other instance of Toxygen uses this profile or profile was not properly closed. Continue?'),
|
|
||||||
QtWidgets.QMessageBox.Yes,
|
|
||||||
QtWidgets.QMessageBox.No)
|
|
||||||
if reply != QtWidgets.QMessageBox.Yes:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
settings.set_active_profile()
|
|
||||||
|
|
||||||
# application color scheme
|
|
||||||
for theme in settings.built_in_themes().keys():
|
|
||||||
if settings['theme'] == theme:
|
|
||||||
with open(curr_directory() + settings.built_in_themes()[theme]) as fl:
|
|
||||||
style = fl.read()
|
|
||||||
app.setStyleSheet(style)
|
|
||||||
|
|
||||||
lang = Settings.supported_languages()[settings['language']]
|
|
||||||
translator = QtCore.QTranslator()
|
|
||||||
translator.load(curr_directory() + '/translations/' + lang)
|
|
||||||
app.installTranslator(translator)
|
|
||||||
app.translator = translator
|
|
||||||
|
|
||||||
# tray icon
|
|
||||||
self.tray = QtWidgets.QSystemTrayIcon(QtGui.QIcon(curr_directory() + '/images/icon.png'))
|
|
||||||
self.tray.setObjectName('tray')
|
|
||||||
|
|
||||||
self.ms = MainWindow(self.tox, self.reset, self.tray)
|
|
||||||
app.aboutToQuit.connect(self.ms.close_window)
|
|
||||||
|
|
||||||
class Menu(QtWidgets.QMenu):
|
|
||||||
|
|
||||||
def newStatus(self, status):
|
|
||||||
if not Settings.get_instance().locked:
|
|
||||||
profile.Profile.get_instance().set_status(status)
|
|
||||||
self.aboutToShowHandler()
|
|
||||||
self.hide()
|
|
||||||
|
|
||||||
def aboutToShowHandler(self):
|
|
||||||
status = profile.Profile.get_instance().status
|
|
||||||
act = self.act
|
|
||||||
if status is None or Settings.get_instance().locked:
|
|
||||||
self.actions()[1].setVisible(False)
|
|
||||||
else:
|
|
||||||
self.actions()[1].setVisible(True)
|
|
||||||
act.actions()[0].setChecked(False)
|
|
||||||
act.actions()[1].setChecked(False)
|
|
||||||
act.actions()[2].setChecked(False)
|
|
||||||
act.actions()[status].setChecked(True)
|
|
||||||
self.actions()[2].setVisible(not Settings.get_instance().locked)
|
|
||||||
|
|
||||||
def languageChange(self, *args, **kwargs):
|
|
||||||
self.actions()[0].setText(QtWidgets.QApplication.translate('tray', 'Open Toxygen'))
|
|
||||||
self.actions()[1].setText(QtWidgets.QApplication.translate('tray', 'Set status'))
|
|
||||||
self.actions()[2].setText(QtWidgets.QApplication.translate('tray', 'Exit'))
|
|
||||||
self.act.actions()[0].setText(QtWidgets.QApplication.translate('tray', 'Online'))
|
|
||||||
self.act.actions()[1].setText(QtWidgets.QApplication.translate('tray', 'Away'))
|
|
||||||
self.act.actions()[2].setText(QtWidgets.QApplication.translate('tray', 'Busy'))
|
|
||||||
|
|
||||||
m = Menu()
|
|
||||||
show = m.addAction(QtWidgets.QApplication.translate('tray', 'Open Toxygen'))
|
|
||||||
sub = m.addMenu(QtWidgets.QApplication.translate('tray', 'Set status'))
|
|
||||||
onl = sub.addAction(QtWidgets.QApplication.translate('tray', 'Online'))
|
|
||||||
away = sub.addAction(QtWidgets.QApplication.translate('tray', 'Away'))
|
|
||||||
busy = sub.addAction(QtWidgets.QApplication.translate('tray', 'Busy'))
|
|
||||||
onl.setCheckable(True)
|
|
||||||
away.setCheckable(True)
|
|
||||||
busy.setCheckable(True)
|
|
||||||
m.act = sub
|
|
||||||
exit = m.addAction(QtWidgets.QApplication.translate('tray', 'Exit'))
|
|
||||||
|
|
||||||
def show_window():
|
|
||||||
s = Settings.get_instance()
|
|
||||||
|
|
||||||
def show():
|
|
||||||
if not self.ms.isActiveWindow():
|
|
||||||
self.ms.setWindowState(self.ms.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
|
|
||||||
self.ms.activateWindow()
|
|
||||||
self.ms.show()
|
|
||||||
if not s.locked:
|
|
||||||
show()
|
|
||||||
else:
|
|
||||||
def correct_pass():
|
|
||||||
show()
|
|
||||||
s.locked = False
|
|
||||||
s.unlockScreen = False
|
|
||||||
if not s.unlockScreen:
|
|
||||||
s.unlockScreen = True
|
|
||||||
self.p = UnlockAppScreen(toxes.ToxES.get_instance(), correct_pass)
|
|
||||||
self.p.show()
|
|
||||||
|
|
||||||
def tray_activated(reason):
|
|
||||||
if reason == QtWidgets.QSystemTrayIcon.DoubleClick:
|
|
||||||
show_window()
|
|
||||||
|
|
||||||
def close_app():
|
|
||||||
if not Settings.get_instance().locked:
|
|
||||||
settings.closing = True
|
|
||||||
self.ms.close()
|
|
||||||
|
|
||||||
show.triggered.connect(show_window)
|
|
||||||
exit.triggered.connect(close_app)
|
|
||||||
m.aboutToShow.connect(lambda: m.aboutToShowHandler())
|
|
||||||
onl.triggered.connect(lambda: m.newStatus(0))
|
|
||||||
away.triggered.connect(lambda: m.newStatus(1))
|
|
||||||
busy.triggered.connect(lambda: m.newStatus(2))
|
|
||||||
|
|
||||||
self.tray.setContextMenu(m)
|
|
||||||
self.tray.show()
|
|
||||||
self.tray.activated.connect(tray_activated)
|
|
||||||
|
|
||||||
self.ms.show()
|
|
||||||
|
|
||||||
updating = False
|
|
||||||
if settings['update'] and updater.updater_available() and updater.connection_available(): # auto update
|
|
||||||
version = updater.check_for_updates()
|
|
||||||
if version is not None:
|
|
||||||
if settings['update'] == 2:
|
|
||||||
updater.download(version)
|
|
||||||
updating = True
|
|
||||||
else:
|
|
||||||
reply = QtWidgets.QMessageBox.question(None,
|
|
||||||
'Toxygen',
|
|
||||||
QtWidgets.QApplication.translate("login",
|
|
||||||
'Update for Toxygen was found. Download and install it?'),
|
|
||||||
QtWidgets.QMessageBox.Yes,
|
|
||||||
QtWidgets.QMessageBox.No)
|
|
||||||
if reply == QtWidgets.QMessageBox.Yes:
|
|
||||||
updater.download(version)
|
|
||||||
updating = True
|
|
||||||
|
|
||||||
if updating:
|
|
||||||
data = self.tox.get_savedata()
|
|
||||||
ProfileHelper.get_instance().save_profile(data)
|
|
||||||
settings.close()
|
|
||||||
del self.tox
|
|
||||||
return
|
|
||||||
|
|
||||||
plugin_helper = PluginLoader(self.tox, settings) # plugin support
|
|
||||||
plugin_helper.load()
|
|
||||||
|
|
||||||
start()
|
|
||||||
# init thread
|
|
||||||
self.init = self.InitThread(self.tox, self.ms, self.tray)
|
|
||||||
self.init.start()
|
|
||||||
|
|
||||||
# starting threads for tox iterate and toxav iterate
|
|
||||||
self.mainloop = self.ToxIterateThread(self.tox)
|
|
||||||
self.mainloop.start()
|
|
||||||
self.avloop = self.ToxAVIterateThread(self.tox.AV)
|
|
||||||
self.avloop.start()
|
|
||||||
|
|
||||||
if self.uri is not None:
|
|
||||||
self.ms.add_contact(self.uri)
|
|
||||||
|
|
||||||
app.lastWindowClosed.connect(app.quit)
|
|
||||||
app.exec_()
|
|
||||||
|
|
||||||
self.init.stop = True
|
|
||||||
self.mainloop.stop = True
|
|
||||||
self.avloop.stop = True
|
|
||||||
plugin_helper.stop()
|
|
||||||
stop()
|
|
||||||
self.mainloop.wait()
|
|
||||||
self.init.wait()
|
|
||||||
self.avloop.wait()
|
|
||||||
self.tray.hide()
|
|
||||||
data = self.tox.get_savedata()
|
|
||||||
ProfileHelper.get_instance().save_profile(data)
|
|
||||||
settings.close()
|
|
||||||
del self.tox
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""
|
|
||||||
Create new tox instance (new network settings)
|
|
||||||
:return: tox instance
|
|
||||||
"""
|
|
||||||
self.mainloop.stop = True
|
|
||||||
self.init.stop = True
|
|
||||||
self.avloop.stop = True
|
|
||||||
self.mainloop.wait()
|
|
||||||
self.init.wait()
|
|
||||||
self.avloop.wait()
|
|
||||||
data = self.tox.get_savedata()
|
|
||||||
ProfileHelper.get_instance().save_profile(data)
|
|
||||||
del self.tox
|
|
||||||
# create new tox instance
|
|
||||||
self.tox = profile.tox_factory(data, Settings.get_instance())
|
|
||||||
# init thread
|
|
||||||
self.init = self.InitThread(self.tox, self.ms, self.tray)
|
|
||||||
self.init.start()
|
|
||||||
|
|
||||||
# starting threads for tox iterate and toxav iterate
|
|
||||||
self.mainloop = self.ToxIterateThread(self.tox)
|
|
||||||
self.mainloop.start()
|
|
||||||
|
|
||||||
self.avloop = self.ToxAVIterateThread(self.tox.AV)
|
|
||||||
self.avloop.start()
|
|
||||||
|
|
||||||
plugin_helper = PluginLoader.get_instance()
|
|
||||||
plugin_helper.set_tox(self.tox)
|
|
||||||
|
|
||||||
return self.tox
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Inner classes
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class InitThread(QtCore.QThread):
|
|
||||||
|
|
||||||
def __init__(self, tox, ms, tray):
|
|
||||||
QtCore.QThread.__init__(self)
|
|
||||||
self.tox, self.ms, self.tray = tox, ms, tray
|
|
||||||
self.stop = False
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# initializing callbacks
|
|
||||||
init_callbacks(self.tox, self.ms, self.tray)
|
|
||||||
# download list of nodes if needed
|
|
||||||
download_nodes_list()
|
|
||||||
# bootstrap
|
|
||||||
try:
|
|
||||||
for data in generate_nodes():
|
|
||||||
if self.stop:
|
|
||||||
return
|
|
||||||
self.tox.bootstrap(*data)
|
|
||||||
self.tox.add_tcp_relay(*data)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
for _ in range(10):
|
|
||||||
if self.stop:
|
|
||||||
return
|
|
||||||
self.msleep(1000)
|
|
||||||
while not self.tox.self_get_connection_status():
|
|
||||||
try:
|
|
||||||
for data in generate_nodes():
|
|
||||||
if self.stop:
|
|
||||||
return
|
|
||||||
self.tox.bootstrap(*data)
|
|
||||||
self.tox.add_tcp_relay(*data)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
self.msleep(5000)
|
|
||||||
|
|
||||||
class ToxIterateThread(QtCore.QThread):
|
|
||||||
|
|
||||||
def __init__(self, tox):
|
|
||||||
QtCore.QThread.__init__(self)
|
|
||||||
self.tox = tox
|
|
||||||
self.stop = False
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while not self.stop:
|
|
||||||
self.tox.iterate()
|
|
||||||
self.msleep(self.tox.iteration_interval())
|
|
||||||
|
|
||||||
class ToxAVIterateThread(QtCore.QThread):
|
|
||||||
|
|
||||||
def __init__(self, toxav):
|
|
||||||
QtCore.QThread.__init__(self)
|
|
||||||
self.toxav = toxav
|
|
||||||
self.stop = False
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while not self.stop:
|
|
||||||
self.toxav.iterate()
|
|
||||||
self.msleep(self.toxav.iteration_interval())
|
|
||||||
|
|
||||||
class Login:
|
|
||||||
|
|
||||||
def __init__(self, arr):
|
|
||||||
self.arr = arr
|
|
||||||
|
|
||||||
def login_screen_close(self, t, number=-1, default=False, name=None):
|
|
||||||
""" Function which processes data from login screen
|
|
||||||
:param t: 0 - window was closed, 1 - new profile was created, 2 - profile loaded
|
|
||||||
:param number: num of chosen profile in list (-1 by default)
|
|
||||||
:param default: was or not chosen profile marked as default
|
|
||||||
:param name: name of new profile
|
|
||||||
"""
|
|
||||||
self.t = t
|
|
||||||
self.num = number
|
|
||||||
self.default = default
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return self.arr[self.num]
|
|
||||||
|
|
||||||
|
|
||||||
def clean():
|
def clean():
|
||||||
"""Removes all windows libs from libs folder"""
|
"""Removes libs folder"""
|
||||||
d = curr_directory() + '/libs/'
|
directory = util.get_libs_directory()
|
||||||
remove(d)
|
util.remove(directory)
|
||||||
|
|
||||||
|
|
||||||
def reset():
|
def reset():
|
||||||
Settings.reset_auto_profile()
|
Settings.reset_auto_profile()
|
||||||
|
|
||||||
|
|
||||||
|
def print_toxygen_version():
|
||||||
|
print('Toxygen v' + __version__)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if len(sys.argv) == 1:
|
parser = argparse.ArgumentParser()
|
||||||
toxygen = Toxygen()
|
parser.add_argument('--version', action='store_true', help='Prints Toxygen version')
|
||||||
else: # started with argument(s)
|
parser.add_argument('--clean', action='store_true', help='Delete toxcore libs from libs folder')
|
||||||
arg = sys.argv[1]
|
parser.add_argument('--reset', action='store_true', help='Reset default profile')
|
||||||
if arg == '--version':
|
parser.add_argument('--uri', help='Add specified Tox ID to friends')
|
||||||
print('Toxygen v' + program_version)
|
parser.add_argument('profile', nargs='?', default=None, help='Path to Tox profile')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.version:
|
||||||
|
print_toxygen_version()
|
||||||
return
|
return
|
||||||
elif arg == '--help':
|
|
||||||
print('Usage:\ntoxygen path_to_profile\ntoxygen tox_id\ntoxygen --version\ntoxygen --reset')
|
if args.clean:
|
||||||
return
|
|
||||||
elif arg == '--clean':
|
|
||||||
clean()
|
clean()
|
||||||
return
|
return
|
||||||
elif arg == '--reset':
|
|
||||||
|
if args.reset:
|
||||||
reset()
|
reset()
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
toxygen = Toxygen(arg)
|
toxygen = app.App(__version__, args.profile, args.uri)
|
||||||
toxygen.main()
|
toxygen.main()
|
||||||
|
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 856 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 515 B After Width: | Height: | Size: 840 B |
Before Width: | Height: | Size: 560 B After Width: | Height: | Size: 519 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |