38 Commits

Author SHA1 Message Date
ac07cb529f reconnection bug fixed 2017-04-22 22:35:32 +03:00
4f77e2c105 @cached 2017-04-17 22:04:22 +03:00
47ce9252b7 Merge pull request #41 from nurupo/fix-md-formatting
Fix markdown formatting
2017-04-12 17:16:06 +03:00
9153836ead Fix markdown formatting 2017-04-12 10:10:48 -04:00
19fb905554 some fixes 2017-04-06 21:28:34 +03:00
3ef581bc5d some messages improvements 2017-04-05 23:46:32 +03:00
f897c7ce8d unlock screen crash fixed 2017-03-27 00:04:32 +03:00
ba390eda91 message splitting bug fix 2017-03-26 18:28:30 +03:00
5fea3e918d plugin reloading refactoring 2017-03-25 18:21:25 +03:00
7cc404ce52 plugins improvements 2017-03-15 23:17:38 +03:00
8a502b4082 block user option in friend menu and translations update 2017-03-08 13:37:19 +03:00
b83ea6be18 reconnection bug fix 2017-03-08 13:19:41 +03:00
85554eacd1 docs updates 2017-03-04 23:35:46 +03:00
8bbefff6c7 history - travis tests fix 2017-03-04 23:18:26 +03:00
019165aeac unsent files fix 2017-03-04 22:15:42 +03:00
0cfb8efefa reconnection fixes 2017-03-03 22:09:45 +03:00
b227ed627a more tests 2017-02-27 21:44:35 +03:00
f41b5e5c97 history test 2017-02-25 23:53:33 +03:00
bc9ec04171 version number fix 2017-02-20 23:47:55 +03:00
05e4184c5d travis fix 2017-02-20 22:03:35 +03:00
3194099f59 default config file moved to app dir 2017-02-20 21:33:04 +03:00
1a9db79ca2 text not found message box 2017-02-13 00:00:41 +03:00
21cc5837cf bug fixes with regex 2017-02-12 23:15:33 +03:00
150942446d fixed bug with html in search and focus 2017-02-12 22:49:08 +03:00
508db0acea ui update for search 2017-02-12 19:46:53 +03:00
de7f3359b8 search in history with regex support 2017-02-12 19:27:38 +03:00
8b56184510 search in history by ctrl + F - initial commit 2017-02-12 17:58:23 +03:00
1d33d298c3 added check 2017-02-11 22:04:32 +03:00
704344fae2 toxes tests fix 2017-02-11 20:23:08 +03:00
3511031aff toxes refactoring 2017-02-11 20:07:28 +03:00
481e48f495 typos fix and todo added 2017-02-07 00:18:57 +03:00
889d3d8f9c 2 minor bug fixes 2017-01-22 00:19:56 +03:00
9b4965d591 bug fixes 2017-01-13 21:08:54 +03:00
5bdbb28e31 reset default profile via --reset 2017-01-04 19:46:23 +03:00
6cafd14883 qtox screenshots support 2016-12-24 22:05:29 +03:00
9d939e7439 avatars handler fix 2016-12-24 14:36:49 +03:00
9e7e9b9012 incorrect contacts list update fixed 2016-12-24 14:20:58 +03:00
2c4301e4f0 bug fixes 2016-11-20 14:12:27 +03:00
35 changed files with 937 additions and 470 deletions

View File

@ -2,6 +2,7 @@ language: python
python:
- "3.4"
before_install:
- sudo apt-get update
- sudo apt-get install -y checkinstall build-essential
- sudo apt-get install portaudio19-dev
- sudo apt-get install libconfig-dev libvpx-dev check -qq

View File

@ -2,13 +2,13 @@
Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pure Python3.
[![Release](https://img.shields.io/github/release/xveduk/toxygen.svg?style=flat)](https://github.com/toxygen-project/toxygen/releases/latest)
[![Stars](https://img.shields.io/github/stars/xveduk/toxygen.svg?style=flat)](https://github.com/toxygen-project/toxygen/stargazers)
[![Open issues](https://img.shields.io/github/issues/xveduk/toxygen.svg?style=flat)](https://github.com/toxygen-project/toxygen/issues)
[![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)
### [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:
@ -18,7 +18,7 @@ Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pu
- [x] 1v1 messages
- [x] File transfers
- [x] Audio
- [x] Audio calls
- [x] Plugins support
- [x] Chat history
- [x] Emoticons
@ -42,7 +42,7 @@ Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pu
- [x] Changing nospam
- [x] File resuming
- [x] Read receipts
- [ ] Video
- [ ] Video calls
- [ ] Desktop sharing
- [ ] Group chats

View File

@ -5,6 +5,7 @@ You can compile Toxygen using [PyInstaller](http://www.pyinstaller.org/)
Install PyInstaller:
``pip3 install pyinstaller``
Compile Toxygen:
``pyinstaller --windowed --icon images/icon.ico main.py``
Don't forget to copy /images/, /sounds/, /translations/, /styles/, /smileys/, /stickers/, /plugins/ (and /libs/libtox.dll, /libs/libsodium.a on Windows) to /dist/main/

View File

@ -1,20 +1,20 @@
#Issues
# Issues
Help us find all bugs in Toxygen! Please provide following info:
- OS
- Toxygen version
- Toxygen executable info - .py or precompiled binary
- Toxygen executable info - .py or precompiled binary, how was it installed in system
- Steps to reproduce the bug
Want to see new feature in Toxygen? [Ask for it!](https://github.com/xveduk/toxygen/issues)
Want to see new feature in Toxygen? [Ask for it!](https://github.com/toxygen-project/toxygen/issues)
#Pull requests
# Pull requests
Developer? Feel free to open pull request. Our dev team is small so we glad to get help.
Don't know what to do? Improve UI, fix [issues](https://github.com/xveduk/toxygen/issues) or implement features from our TODO list.
Don't know what to do? Improve UI, fix [issues](https://github.com/toxygen-project/toxygen/issues) or implement features from our TODO list.
You can find our TODO's in code, issues list and [here](/README.md). Also you can implement [plugins](/docs/plugins.md) for Toxygen.
#Translations
# Translations
Help us translate Toxygen! Translation can be created using pyside-lupdate (``pyside-lupdate toxygen.pro``) and QT Linguist.

View File

@ -1,7 +1,7 @@
# How to install Toxygen
## Use precompiled binary:
[Check our releases page](https://github.com/xveduk/toxygen/releases)
[Check our releases page](https://github.com/toxygen-project/toxygen/releases)
## Using pip3

View File

@ -1,4 +1,4 @@
#Plugins API
# Plugins API
In Toxygen plugin is single python (supported Python 3.0 - 3.4) module (.py file) and directory with data associated with it.
Every module must contain one class derived from PluginSuperClass defined in [plugin_super_class.py](/src/plugins/plugin_super_class.py). Instance of this class will be created by PluginLoader class (defined in [plugin_support.py](/src/plugin_support.py) ). This class can enable/disable plugins and send data to it.
@ -18,7 +18,7 @@ All plugin's data should be stored in following structure:
```
Plugin MUST override:
- __init__ with params: tox (Tox instance), profile (Profile instance), settings (Settings instance), encrypt_save (ToxEncryptSave instance). Call super().__init__ with params plugin_full_name, plugin_short_name, tox, profile, settings, encrypt_save.
- __init__ with params: tox (Tox instance), profile (Profile instance), settings (Settings instance), encrypt_save (ToxES instance). Call super().__init__ with params plugin_full_name, plugin_short_name, tox, profile, settings, encrypt_save.
Plugin can override following methods:
- get_description - this method should return plugin description.
@ -51,7 +51,7 @@ Exceptions:
Plugin's methods MUST NOT raise exceptions.
#Examples
# Examples
You can find examples in [official repo](https://github.com/ingvar1995/toxygen_plugins)
You can find examples in [official repo](https://github.com/toxygen-project/toxygen_plugins)

View File

@ -1,22 +1,22 @@
#Plugins
# Plugins
Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python 3.4 module (.py file) and directory with plugin's data which provide some additional functionality.
#How to write plugin
# How to write plugin
Check [Plugin API](/docs/plugin_api.md) for more info
#How to install plugin
# How to install plugin
Toxygen comes without preinstalled plugins.
1. Put plugin and directory with its data into /src/plugins/ or import it via GUI (In menu: Plugins -> Import plugin)
2. Restart Toxygen
1. Put plugin and directory with its data into /src/plugins/ or import it via GUI (In menu: Plugins => Import plugin)
2. Restart Toxygen or choose Plugins => Reload plugins in menu.
##Note: /src/plugins/ should contain plugin_super_class.py and __init__.py
## Note: /src/plugins/ should contain plugin_super_class.py and __init__.py
#Plugins list
# Plugins list
WARNING: It is unsecure to install plugin not from this list!
[Main repo](https://github.com/ingvar1995/toxygen_plugins)
[Main repo](https://github.com/toxygen-project/toxygen_plugins)

View File

@ -1,4 +1,4 @@
#Smileys
# Smileys
Toxygen support smileys. Smiley is small picture which replaces some symbol or combination of symbols. If you want to create your own smiley pack, create directory in src/smileys/. This directory must contain images with smileys and config.json. Example of config.json:
@ -6,8 +6,8 @@ Toxygen support smileys. Smiley is small picture which replaces some symbol or c
Animated smileys (.gif) are supported too.
#Stickers
# Stickers
Sticker is inline image. If you want to create your own smiley pack, create directory in src/stickers/ and place your stickers there.
Users can import plugins and stickers packs using menu: Settings -> Interface
Users can import smileys and stickers using menu: Settings -> Interface

View File

@ -1,7 +1,10 @@
from toxygen.profile import *
from toxygen.tox_dns import tox_dns
import toxygen.toxencryptsave as encr
import toxygen.messages as m
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
@ -53,61 +56,122 @@ class TestEncryption:
def test_encr_decr(self):
tox = tox_factory()
data = tox.get_savedata()
lib = encr.ToxEncryptSave()
for password in ('easypassword', 'njvnjfnvaGGV6', 'toxygen'):
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 create_singletons(self):
Settings._instance = Settings.get_default_settings()
ProfileHelper('abc', 'test')
def create_friend(self, name, status_message, number, tox_id):
friend = Friend(None, number, name, status_message, None, tox_id)
return friend
def test_friend_creation(self):
self.create_singletons()
create_singletons()
name, status_message, number = 'Friend', 'I am friend!', 0
tox_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6'
friend = self.create_friend(name, status_message, number, tox_id)
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):
self.create_singletons()
name, status_message, number = 'Friend', 'I am friend!', 0
tox_id = '76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6'
friend = self.create_friend(name, status_message, number, tox_id)
create_singletons()
friend = create_random_friend()
t = time.time()
friend.append_message(m.InfoMessage('Info message', t))
friend.append_message(m.TextMessage('Hello! It is test!', MESSAGE_OWNER['ME'], t + 0.001, 0))
friend.append_message(m.TextMessage('Hello!', MESSAGE_OWNER['FRIEND'], t + 0.002, 0))
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(m.TextMessage('Not sent', MESSAGE_OWNER['NOT_SENT'], t + 0.002, 0))
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 = m.TransferMessage(MESSAGE_OWNER['FRIEND'],
time.time(),
TOX_FILE_TRANSFER_STATE['RUNNING'],
100, 'file_name', friend.number, 0)
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(m.TextMessage('Hello! It is test!', MESSAGE_OWNER['ME'], t, 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
# TODO: more friend tests and history test
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)

View File

@ -63,6 +63,7 @@ class IncomingCallWidget(widgets.CenteredWidget):
def __init__(self):
QtCore.QThread.__init__(self)
self.a = None
def run(self):
class AudioFile:

View File

@ -84,11 +84,10 @@ class BaseContact:
"""
Tries to load avatar of contact or uses default avatar
"""
avatar_path = '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
os.chdir(ProfileHelper.get_path() + 'avatars/')
if not os.path.isfile(avatar_path): # load default image
avatar_path = 'avatar.png'
os.chdir(curr_directory() + '/images/')
prefix = ProfileHelper.get_path() + 'avatars/'
avatar_path = prefix + '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
if not os.path.isfile(avatar_path) or not os.path.getsize(avatar_path): # load default image
avatar_path = curr_directory() + '/images/avatar.png'
width = self._widget.avatar_label.width()
pixmap = QtGui.QPixmap(avatar_path)
self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio,
@ -110,6 +109,10 @@ class BaseContact:
def get_pixmap(self):
return self._widget.avatar_label.pixmap()
# -----------------------------------------------------------------------------------------------------------------
# Widgets
# -----------------------------------------------------------------------------------------------------------------
def init_widget(self):
if self._widget is not None:
self._widget.name.setText(self._name)

View File

@ -141,4 +141,3 @@ class AV:
if state & TOXAV_FRIEND_CALL_STATE['ACCEPTING_A']:
self._calls[friend_number] |= 1

View File

@ -7,6 +7,7 @@ import basecontact
import util
from messages import *
import file_transfers as ft
import re
class Contact(basecontact.BaseContact):
@ -29,8 +30,8 @@ class Contact(basecontact.BaseContact):
self._corr = []
self._unsaved_messages = 0
self._history_loaded = self._new_actions = False
self._receipts = 0
self._curr_text = ''
self._curr_text = self._search_string = ''
self._search_index = 0
def __del__(self):
self.set_visibility(False)
@ -48,6 +49,8 @@ class Contact(basecontact.BaseContact):
"""
if (first_time and self._history_loaded) or (not hasattr(self, '_message_getter')):
return
if self._message_getter is None:
return
data = list(self._message_getter.get(PAGE_SIZE))
if data is not None and len(data):
data.reverse()
@ -94,6 +97,10 @@ class Contact(basecontact.BaseContact):
else:
return ''
# -----------------------------------------------------------------------------------------------------------------
# Unsent messages
# -----------------------------------------------------------------------------------------------------------------
def get_unsent_messages(self):
"""
:return list of unsent messages
@ -108,25 +115,6 @@ class Contact(basecontact.BaseContact):
messages = filter(lambda x: x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr)
return list(map(lambda x: x.get_data(), messages))
def delete_message(self, time):
elem = list(filter(lambda x: type(x) is TextMessage and x.get_data()[2] == time, self._corr))[0]
tmp = list(filter(lambda x: x.get_type() <= 1, self._corr))
if elem in tmp[-self._unsaved_messages:] and self._unsaved_messages:
self._unsaved_messages -= 1
self._corr.remove(elem)
self._message_getter.delete_one()
def delete_old_messages(self):
"""
Delete old messages (reduces RAM if messages saving is not enabled)
"""
old = filter(lambda x: x.get_type() == 2 and (x.get_status() >= 2 or x.get_status() is None),
self._corr[:-SAVE_MESSAGES])
old = list(old)
l = max(len(self._corr) - SAVE_MESSAGES, 0) - len(old)
self._unsaved_messages -= l
self._corr = old + self._corr[-SAVE_MESSAGES:]
def mark_as_sent(self):
try:
message = list(filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr))[0]
@ -134,12 +122,41 @@ class Contact(basecontact.BaseContact):
except Exception as ex:
util.log('Mark as sent ex: ' + str(ex))
# -----------------------------------------------------------------------------------------------------------------
# Message deletion
# -----------------------------------------------------------------------------------------------------------------
def delete_message(self, time):
elem = list(filter(lambda x: type(x) is TextMessage and x.get_data()[2] == time, self._corr))[0]
tmp = list(filter(lambda x: x.get_type() <= 1, self._corr))
if elem in tmp[-self._unsaved_messages:] and self._unsaved_messages:
self._unsaved_messages -= 1
self._corr.remove(elem)
self._message_getter.delete_one()
self._search_index = 0
def delete_old_messages(self):
"""
Delete old messages (reduces RAM usage if messages saving is not enabled)
"""
def save_message(x):
if x.get_type() == 2 and (x.get_status() >= 2 or x.get_status() is None):
return True
return x.get_owner() == MESSAGE_OWNER['NOT_SENT']
old = filter(save_message, self._corr[:-SAVE_MESSAGES])
self._corr = list(old) + self._corr[-SAVE_MESSAGES:]
text_messages = filter(lambda x: x.get_type() <= 1, self._corr)
self._unsaved_messages = min(self._unsaved_messages, len(list(text_messages)))
self._search_index = 0
def clear_corr(self, save_unsent=False):
"""
Clear messages list
"""
if hasattr(self, '_message_getter'):
del self._message_getter
self._search_index = 0
# don't delete data about active file transfer
if not save_unsent:
self._corr = list(filter(lambda x: x.get_type() == 2 and
@ -151,6 +168,45 @@ class Contact(basecontact.BaseContact):
self._corr))
self._unsaved_messages = len(self.get_unsent_messages())
# -----------------------------------------------------------------------------------------------------------------
# Chat history search
# -----------------------------------------------------------------------------------------------------------------
def search_string(self, search_string):
self._search_string, self._search_index = search_string, 0
return self.search_prev()
def search_prev(self):
while True:
l = len(self._corr)
for i in range(self._search_index - 1, -l - 1, -1):
if self._corr[i].get_type() > 1:
continue
message = self._corr[i].get_data()[0]
if re.search(self._search_string, message, re.IGNORECASE) is not None:
self._search_index = i
return i
self._search_index = -l
self.load_corr(False)
if len(self._corr) == l:
return None # not found
def search_next(self):
if not self._search_index:
return None
for i in range(self._search_index + 1, 0):
if self._corr[i].get_type() > 1:
continue
message = self._corr[i].get_data()[0]
if re.search(self._search_string, message, re.IGNORECASE) is not None:
self._search_index = i
return i
return None # not found
# -----------------------------------------------------------------------------------------------------------------
# Current text - text from message area
# -----------------------------------------------------------------------------------------------------------------
def get_curr_text(self):
return self._curr_text

View File

@ -32,6 +32,10 @@ SHOW_PROGRESS_BAR = (0, 1, 4)
ALLOWED_FILES = ('toxygen_inline.png', 'utox-inline.png', 'sticker.png')
def is_inline(file_name):
return file_name in ALLOWED_FILES or file_name.startswith('qTox_Screenshot_')
class StateSignal(QtCore.QObject):
signal = QtCore.Signal(int, float, int) # state, progress, time in sec

View File

@ -1,5 +1,6 @@
import contact
from messages import *
import os
class Friend(contact.Contact):
@ -37,6 +38,15 @@ class Friend(contact.Contact):
def clear_unsent_files(self):
self._corr = list(filter(lambda x: type(x) is not UnsentFile, self._corr))
def remove_invalid_unsent_files(self):
def is_valid(message):
if type(message) is not UnsentFile:
return True
if message.get_data()[1] is not None:
return True
return os.path.exists(message.get_data()[0])
self._corr = list(filter(is_valid, self._corr))
def delete_one_unsent_file(self, time):
self._corr = list(filter(lambda x: not (type(x) is UnsentFile and x.get_data()[2] == time), self._corr))

View File

@ -1,15 +1,18 @@
# -*- coding: utf-8 -*-
from sqlite3 import connect
import settings
from os import chdir
import os.path
from toxencryptsave import ToxEncryptSave
from toxes import ToxES
PAGE_SIZE = 42
TIMEOUT = 11
SAVE_MESSAGES = 150
SAVE_MESSAGES = 250
MESSAGE_OWNER = {
'ME': 0,
@ -25,7 +28,7 @@ class History:
chdir(settings.ProfileHelper.get_path())
path = settings.ProfileHelper.get_path() + self._name + '.hstr'
if os.path.exists(path):
decr = ToxEncryptSave.get_instance()
decr = ToxES.get_instance()
try:
with open(path, 'rb') as fin:
data = fin.read()
@ -43,7 +46,7 @@ class History:
db.close()
def save(self):
encr = ToxEncryptSave.get_instance()
encr = ToxES.get_instance()
if encr.has_password():
path = settings.ProfileHelper.get_path() + self._name + '.hstr'
with open(path, 'rb') as fin:
@ -57,7 +60,7 @@ class History:
new_path = directory + self._name + '.hstr'
with open(path, 'rb') as fin:
data = fin.read()
encr = ToxEncryptSave.get_instance()
encr = ToxES.get_instance()
if encr.has_password():
data = encr.pass_encrypt(data)
with open(new_path, 'wb') as fout:
@ -133,14 +136,15 @@ class History:
db.rollback()
finally:
db.close()
pass
def delete_message(self, tox_id, time):
start, end = str(time - 0.01), str(time + 0.01)
chdir(settings.ProfileHelper.get_path())
db = connect(self._name + '.hstr', timeout=TIMEOUT)
try:
cursor = db.cursor()
cursor.execute('DELETE FROM id' + tox_id + ' WHERE unix_time = ' + str(time) + ';')
cursor.execute('DELETE FROM id' + tox_id + ' WHERE unix_time < ' + end + ' AND unix_time > ' +
start + ';')
db.commit()
except:
print('Database is locked!')

View File

@ -11,6 +11,7 @@ from widgets import DataLabel, create_menu
import html as h
import smileys
import settings
import re
class MessageEdit(QtGui.QTextBrowser):
@ -189,6 +190,31 @@ class MessageItem(QtGui.QWidget):
self.message.setFixedHeight(self.height())
self.name.setPixmap(pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
def select_text(self, text):
tmp = self.message.toHtml()
text = h.escape(text)
strings = re.findall(text, tmp, flags=re.IGNORECASE)
for s in strings:
tmp = self.replace_all(tmp, s)
self.message.setHtml(tmp)
@staticmethod
def replace_all(text, substring):
i, l = 0, len(substring)
while i < len(text) - l + 1:
index = text[i:].find(substring)
if index == -1:
break
i += index
lgt, rgt = text[i:].find('<'), text[i:].find('>')
if rgt < lgt:
i += rgt + 1
continue
sub = '<font color="red"><b>{}</b></font>'.format(substring)
text = text[:i] + sub + text[i + l:]
i += len(sub)
return text
class ContactItem(QtGui.QWidget):
"""

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import sys
from loginscreen import LoginScreen
import profile
@ -12,7 +14,7 @@ from callbacks import init_callbacks, stop, start
from util import curr_directory, program_version, remove, is_64_bit
import styles.style
import platform
import toxencryptsave
import toxes
from passwordscreen import PasswordScreen, UnlockAppScreen, SetProfilePasswordScreen
from plugin_support import PluginLoader
import updater
@ -37,7 +39,7 @@ class Toxygen:
Show password screen
"""
tmp = [data]
p = PasswordScreen(toxencryptsave.ToxEncryptSave.get_instance(), tmp)
p = PasswordScreen(toxes.ToxES.get_instance(), tmp)
p.show()
self.app.connect(self.app, QtCore.SIGNAL("lastWindowClosed()"), self.app, QtCore.SLOT("quit()"))
self.app.exec_()
@ -62,7 +64,7 @@ class Toxygen:
dark_style = fl.read()
app.setStyleSheet(dark_style)
encrypt_save = toxencryptsave.ToxEncryptSave()
encrypt_save = toxes.ToxES()
if self.path is not None:
path = os.path.dirname(self.path) + '/'
@ -199,9 +201,10 @@ class Toxygen:
class Menu(QtGui.QMenu):
def newStatus(self, status):
profile.Profile.get_instance().set_status(status)
self.aboutToShow()
self.hide()
if not Settings.get_instance().locked:
profile.Profile.get_instance().set_status(status)
self.aboutToShow()
self.hide()
def aboutToShow(self):
status = profile.Profile.get_instance().status
@ -237,27 +240,33 @@ class Toxygen:
exit = m.addAction(QtGui.QApplication.translate('tray', 'Exit', None, QtGui.QApplication.UnicodeUTF8))
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 Settings.get_instance().locked:
if not s.locked:
show()
else:
def correct_pass():
show()
Settings.get_instance().locked = False
self.p = UnlockAppScreen(toxencryptsave.ToxEncryptSave.get_instance(), correct_pass)
self.p.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 == QtGui.QSystemTrayIcon.DoubleClick:
show_window()
def close_app():
settings.closing = True
self.ms.close()
if not Settings.get_instance().locked:
settings.closing = True
self.ms.close()
m.connect(show, QtCore.SIGNAL("triggered()"), show_window)
m.connect(exit, QtCore.SIGNAL("triggered()"), close_app)
@ -475,6 +484,10 @@ def configure():
pass
def reset():
Settings.reset_auto_profile()
def main():
if len(sys.argv) == 1:
toxygen = Toxygen()
@ -484,7 +497,7 @@ def main():
print('Toxygen v' + program_version)
return
elif arg == '--help':
print('Usage:\ntoxygen path_to_profile\ntoxygen tox_id\ntoxygen --version')
print('Usage:\ntoxygen path_to_profile\ntoxygen tox_id\ntoxygen --version\ntoxygen --reset')
return
elif arg == '--configure':
configure()
@ -492,6 +505,9 @@ def main():
elif arg == '--clean':
clean()
return
elif arg == '--reset':
reset()
return
else:
toxygen = Toxygen(arg)
toxygen.main()

View File

@ -5,6 +5,8 @@ from widgets import MultilineEdit, LineEdit, ComboBox
import plugin_support
from mainscreen_widgets import *
import settings
import platform
import toxes
class MainWindow(QtGui.QMainWindow, Singleton):
@ -63,6 +65,7 @@ class MainWindow(QtGui.QMainWindow, Singleton):
self.audioSettings = QtGui.QAction(Form)
self.pluginData = QtGui.QAction(Form)
self.importPlugin = QtGui.QAction(Form)
self.reloadPlugins = QtGui.QAction(Form)
self.lockApp = QtGui.QAction(Form)
self.menuProfile.addAction(self.actionAdd_friend)
self.menuProfile.addAction(self.actionSettings)
@ -75,6 +78,7 @@ class MainWindow(QtGui.QMainWindow, Singleton):
self.menuSettings.addAction(self.updateSettings)
self.menuPlugins.addAction(self.pluginData)
self.menuPlugins.addAction(self.importPlugin)
self.menuPlugins.addAction(self.reloadPlugins)
self.menuAbout.addAction(self.actionAbout_program)
self.profile_button.setMenu(self.menuProfile)
@ -94,6 +98,7 @@ class MainWindow(QtGui.QMainWindow, Singleton):
self.pluginData.triggered.connect(self.plugins_menu)
self.lockApp.triggered.connect(self.lock_app)
self.importPlugin.triggered.connect(self.import_plugin)
self.reloadPlugins.triggered.connect(self.reload_plugins)
Form.setLayout(box)
QtCore.QMetaObject.connectSlotsByName(Form)
@ -138,6 +143,7 @@ class MainWindow(QtGui.QMainWindow, Singleton):
d = {0: 0, 1: 1, 2: 2, 3: 4, 1 | 4: 4, 2 | 4: 5}
self.online_contacts.setCurrentIndex(d[ind])
self.importPlugin.setText(QtGui.QApplication.translate("MainWindow", "Import plugin", None, QtGui.QApplication.UnicodeUTF8))
self.reloadPlugins.setText(QtGui.QApplication.translate("MainWindow", "Reload plugins", None, QtGui.QApplication.UnicodeUTF8))
def setup_right_bottom(self, Form):
Form.resize(650, 60)
@ -374,8 +380,12 @@ class MainWindow(QtGui.QMainWindow, Singleton):
self.close()
def resizeEvent(self, *args, **kwargs):
self.messages.setGeometry(0, 0, self.width() - 270, self.height() - 159)
self.friends_list.setGeometry(0, 0, 270, self.height() - 129)
if platform.system() == 'Windows':
self.messages.setGeometry(0, 0, self.width() - 270, self.height() - 155)
self.friends_list.setGeometry(0, 0, 270, self.height() - 125)
else:
self.messages.setGeometry(0, 0, self.width() - 270, self.height() - 159)
self.friends_list.setGeometry(0, 0, 270, self.height() - 129)
self.videocallButton.setGeometry(QtCore.QRect(self.width() - 330, 10, 50, 50))
self.callButton.setGeometry(QtCore.QRect(self.width() - 390, 10, 50, 50))
@ -401,6 +411,8 @@ class MainWindow(QtGui.QMainWindow, Singleton):
clipboard.setText(s)
elif event.key() == QtCore.Qt.Key_Z and event.modifiers() & QtCore.Qt.ControlModifier and self.messages.selectedIndexes():
self.messages.clearSelection()
elif event.key() == QtCore.Qt.Key_F and event.modifiers() & QtCore.Qt.ControlModifier:
self.show_search_field()
else:
super(MainWindow, self).keyPressEvent(event)
@ -452,6 +464,11 @@ class MainWindow(QtGui.QMainWindow, Singleton):
self.update_s = UpdateSettings()
self.update_s.show()
def reload_plugins(self):
plugin_loader = plugin_support.PluginLoader.get_instance()
if plugin_loader is not None:
plugin_loader.reload()
def import_plugin(self):
import util
directory = QtGui.QFileDialog.getExistingDirectory(self,
@ -473,7 +490,7 @@ class MainWindow(QtGui.QMainWindow, Singleton):
msgBox.exec_()
def lock_app(self):
if toxencryptsave.ToxEncryptSave.get_instance().has_password():
if toxes.ToxES.get_instance().has_password():
Settings.get_instance().locked = True
self.hide()
else:
@ -588,14 +605,18 @@ class MainWindow(QtGui.QMainWindow, Singleton):
auto_accept_item = self.listMenu.addAction(auto)
remove_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Remove friend', None, QtGui.QApplication.UnicodeUTF8))
block_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Block friend', None, QtGui.QApplication.UnicodeUTF8))
notes_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Notes', None, QtGui.QApplication.UnicodeUTF8))
submenu = plugin_support.PluginLoader.get_instance().get_menu(self.listMenu, num)
if len(submenu):
plug = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Plugins', None, QtGui.QApplication.UnicodeUTF8))
plug.addActions(submenu)
plugins_loader = plugin_support.PluginLoader.get_instance()
if plugins_loader is not None:
submenu = plugins_loader.get_menu(self.listMenu, num)
if len(submenu):
plug = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Plugins', None, QtGui.QApplication.UnicodeUTF8))
plug.addActions(submenu)
self.connect(set_alias_item, QtCore.SIGNAL("triggered()"), lambda: self.set_alias(num))
self.connect(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(num))
self.connect(block_item, QtCore.SIGNAL("triggered()"), lambda: self.block_friend(num))
self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num))
self.connect(clear_history_item, QtCore.SIGNAL("triggered()"), lambda: self.clear_history(num))
self.connect(auto_accept_item, QtCore.SIGNAL("triggered()"), lambda: self.auto_accept(num, not allowed))
@ -644,6 +665,10 @@ class MainWindow(QtGui.QMainWindow, Singleton):
def remove_friend(self, num):
self.profile.delete_friend(num)
def block_friend(self, num):
friend = self.profile.get_friend(num)
self.profile.block_user(friend.tox_id)
def copy_friend_key(self, num):
tox_id = self.profile.friend_public_key(num)
clipboard = QtGui.QApplication.clipboard()
@ -685,7 +710,22 @@ class MainWindow(QtGui.QMainWindow, Singleton):
else:
super(MainWindow, self).mouseReleaseEvent(event)
def show(self):
super().show()
self.profile.update()
def filtering(self):
ind = self.online_contacts.currentIndex()
d = {0: 0, 1: 1, 2: 2, 3: 4, 4: 1 | 4, 5: 2 | 4}
self.profile.filtration_and_sorting(d[ind], self.contact_name.text())
def show_search_field(self):
if hasattr(self, 'search_field') and self.search_field.isVisible():
return
if self.profile.get_curr_friend() is None:
return
self.search_field = SearchScreen(self.messages, self.messages.width(), self.messages.parent())
x, y = self.messages.x(), self.messages.y() + self.messages.height() - 40
self.search_field.setGeometry(x, y, self.messages.width(), 40)
self.messages.setGeometry(x, self.messages.y(), self.messages.width(), self.messages.height() - 40)
self.search_field.show()

View File

@ -2,7 +2,7 @@ try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
from widgets import RubberBand, create_menu, QRightClickButton, CenteredWidget
from widgets import RubberBand, create_menu, QRightClickButton, CenteredWidget, LineEdit
from profile import Profile
import smileys
import util
@ -363,11 +363,7 @@ class WelcomeScreen(CenteredWidget):
text = QtGui.QApplication.translate('WelcomeScreen',
'Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/xveduk/toxygen/blob/master/docs/plugins.md">Read more</a>',
None, QtGui.QApplication.UnicodeUTF8)
elif num == 6:
text = QtGui.QApplication.translate('WelcomeScreen',
'New in Toxygen v0.2.6:<br>Updater<br>Better contact sorting<br>Plugins improvements',
None, QtGui.QApplication.UnicodeUTF8)
elif num == 7:
elif num in (6, 7):
text = QtGui.QApplication.translate('WelcomeScreen',
'Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.',
None, QtGui.QApplication.UnicodeUTF8)
@ -404,3 +400,130 @@ class MainMenuButton(QtGui.QPushButton):
metrics = QtGui.QFontMetrics(self.font())
self.setFixedWidth(metrics.size(QtCore.Qt.TextSingleLine, text).width() + 20)
super().setText(text)
class ClickableLabel(QtGui.QLabel):
def __init__(self, *args):
super().__init__(*args)
def mouseReleaseEvent(self, ev):
self.emit(QtCore.SIGNAL('clicked()'))
class SearchScreen(QtGui.QWidget):
def __init__(self, messages, width, *args):
super().__init__(*args)
self.setMaximumSize(width, 40)
self.setMinimumSize(width, 40)
self._messages = messages
self.search_text = LineEdit(self)
self.search_text.setGeometry(0, 0, width - 160, 40)
self.search_button = ClickableLabel(self)
self.search_button.setGeometry(width - 160, 0, 40, 40)
pixmap = QtGui.QPixmap()
pixmap.load(util.curr_directory() + '/images/search.png')
self.search_button.setScaledContents(False)
self.search_button.setAlignment(QtCore.Qt.AlignCenter)
self.search_button.setPixmap(pixmap)
self.connect(self.search_button, QtCore.SIGNAL('clicked()'), self.search)
font = QtGui.QFont()
font.setPointSize(32)
font.setBold(True)
self.prev_button = QtGui.QPushButton(self)
self.prev_button.setGeometry(width - 120, 0, 40, 40)
self.prev_button.clicked.connect(self.prev)
self.prev_button.setText('\u25B2')
self.next_button = QtGui.QPushButton(self)
self.next_button.setGeometry(width - 80, 0, 40, 40)
self.next_button.clicked.connect(self.next)
self.next_button.setText('\u25BC')
self.close_button = QtGui.QPushButton(self)
self.close_button.setGeometry(width - 40, 0, 40, 40)
self.close_button.clicked.connect(self.close)
self.close_button.setText('×')
self.close_button.setFont(font)
font.setPointSize(18)
self.next_button.setFont(font)
self.prev_button.setFont(font)
self.retranslateUi()
def retranslateUi(self):
self.search_text.setPlaceholderText(QtGui.QApplication.translate("MainWindow", "Search", None,
QtGui.QApplication.UnicodeUTF8))
def show(self):
super().show()
self.search_text.setFocus()
def search(self):
Profile.get_instance().update()
text = self.search_text.text()
friend = Profile.get_instance().get_curr_friend()
if text and friend and util.is_re_valid(text):
index = friend.search_string(text)
self.load_messages(index)
def prev(self):
friend = Profile.get_instance().get_curr_friend()
if friend is not None:
index = friend.search_prev()
self.load_messages(index)
def next(self):
friend = Profile.get_instance().get_curr_friend()
text = self.search_text.text()
if friend is not None:
index = friend.search_next()
if index is not None:
count = self._messages.count()
index += count
item = self._messages.item(index)
self._messages.scrollToItem(item)
self._messages.itemWidget(item).select_text(text)
else:
self.not_found(text)
def load_messages(self, index):
text = self.search_text.text()
if index is not None:
profile = Profile.get_instance()
count = self._messages.count()
while count + index < 0:
profile.load_history()
count = self._messages.count()
index += count
item = self._messages.item(index)
self._messages.scrollToItem(item)
self._messages.itemWidget(item).select_text(text)
else:
self.not_found(text)
def closeEvent(self, *args):
Profile.get_instance().update()
self._messages.setGeometry(0, 0, self._messages.width(), self._messages.height() + 40)
super().closeEvent(*args)
@staticmethod
def not_found(text):
mbox = QtGui.QMessageBox()
mbox_text = QtGui.QApplication.translate("MainWindow",
'Text "{}" was not found',
None,
QtGui.QApplication.UnicodeUTF8)
mbox.setText(mbox_text.format(text))
mbox.setWindowTitle(QtGui.QApplication.translate("MainWindow",
'Not found',
None,
QtGui.QApplication.UnicodeUTF8))
mbox.exec_()

View File

@ -7,7 +7,7 @@ from profile import Profile
from util import curr_directory, copy
from widgets import CenteredWidget, DataLabel, LineEdit
import pyaudio
import toxencryptsave
import toxes
import plugin_support
import updater
@ -215,7 +215,7 @@ class ProfileSettings(CenteredWidget):
def new_password(self):
if self.password.text() == self.confirm_password.text():
if not len(self.password.text()) or len(self.password.text()) >= 8:
e = toxencryptsave.ToxEncryptSave.get_instance()
e = toxes.ToxES.get_instance()
e.set_password(self.password.text())
self.close()
else:

View File

@ -4,7 +4,7 @@ import os
import importlib
import inspect
import plugins.plugin_super_class as pl
import toxencryptsave
import toxes
import sys
@ -16,7 +16,7 @@ class PluginLoader(util.Singleton):
self._settings = settings
self._plugins = {} # dict. key - plugin unique short name, value - tuple (plugin instance, is active)
self._tox = tox
self._encr = toxencryptsave.ToxEncryptSave.get_instance()
self._encr = toxes.ToxES.get_instance()
def set_tox(self, tox):
"""
@ -51,7 +51,8 @@ class PluginLoader(util.Singleton):
continue
for elem in dir(module):
obj = getattr(module, elem)
if inspect.isclass(obj) and hasattr(obj, 'is_plugin') and obj.is_plugin: # looking for plugin class in module
# looking for plugin class in module
if inspect.isclass(obj) and hasattr(obj, 'is_plugin') and obj.is_plugin:
print('Plugin', elem)
try: # create instance of plugin class
inst = obj(self._tox, self._profile, self._settings, self._encr)
@ -165,3 +166,8 @@ class PluginLoader(util.Singleton):
if self._plugins[key][1]:
self._plugins[key][0].close()
del self._plugins[key]
def reload(self):
print('Reloading plugins')
self.stop()
self.load()

View File

@ -31,7 +31,7 @@ def log(name, data):
class PluginSuperClass:
"""
Superclass for all plugins. Plugin is python module with at least one class derived from PluginSuperClass.
Superclass for all plugins. Plugin is Python3 module with at least one class derived from PluginSuperClass.
"""
is_plugin = True
@ -43,7 +43,7 @@ class PluginSuperClass:
:param tox: tox instance
:param profile: profile instance
:param settings: profile settings
:param encrypt_save: LibToxEncryptSave instance.
:param encrypt_save: ToxES instance.
"""
self._settings = settings
self._profile = profile
@ -169,7 +169,7 @@ class PluginSuperClass:
def load_settings(self):
"""
This method loads settings of plugin and returns raw data
If file doesn't this method raises exception
If file doesn't exist this method raises exception
"""
with open(path_to_data(self._short_name) + 'settings.json', 'rb') as fl:
data = fl.read()

View File

@ -41,6 +41,7 @@ class Profile(basecontact.BaseContact, Singleton):
self._call = calls.AV(tox.AV) # object with data about calls
self._incoming_calls = set()
self._load_history = True
self._waiting_for_reconnection = False
self._factory = items_factory.ItemsFactory(self._screen.friends_list, self._messages)
settings = Settings.get_instance()
self._sorting = settings['sorting']
@ -86,8 +87,9 @@ class Profile(basecontact.BaseContact, Singleton):
super(Profile, self).set_status(status)
if status is not None:
self._tox.self_set_status(status)
else:
QtCore.QTimer.singleShot(45000, self.reconnect)
elif not self._waiting_for_reconnection:
self._waiting_for_reconnection = True
QtCore.QTimer.singleShot(50000, self.reconnect)
def set_name(self, value):
if self.name == value:
@ -127,6 +129,7 @@ class Profile(basecontact.BaseContact, Singleton):
"""
filter_str = filter_str.lower()
settings = Settings.get_instance()
number = self.get_active_number()
if sorting > 1:
if sorting & 2:
self._contacts = sorted(self._contacts, key=lambda x: int(x.status is not None), reverse=True)
@ -162,6 +165,7 @@ class Profile(basecontact.BaseContact, Singleton):
self._sorting, self._filter_string = sorting, filter_str
settings['sorting'] = self._sorting
settings.save()
self.set_active_by_number(number)
def update_filtration(self):
"""
@ -181,6 +185,9 @@ class Profile(basecontact.BaseContact, Singleton):
return None
return self._contacts[num]
def get_curr_friend(self):
return self._contacts[self._active_friend] if self._active_friend + 1 else None
# -----------------------------------------------------------------------------------------------------------------
# Work with active friend
# -----------------------------------------------------------------------------------------------------------------
@ -209,10 +216,11 @@ class Profile(basecontact.BaseContact, Singleton):
if value is not None:
if self._active_friend + 1 and self._active_friend != value:
try:
self._contacts[self._active_friend].curr_text = self._screen.messageEdit.toPlainText()
self.get_curr_friend().curr_text = self._screen.messageEdit.toPlainText()
except:
pass
friend = self._contacts[value]
friend.remove_invalid_unsent_files()
if self._active_friend != value:
self._screen.messageEdit.setPlainText(friend.curr_text)
self._active_friend = value
@ -259,7 +267,7 @@ class Profile(basecontact.BaseContact, Singleton):
else:
self._screen.call_finished()
else:
friend = self._contacts[self._active_friend]
friend = self.get_curr_friend()
self._screen.account_name.setText(friend.name)
self._screen.account_status.setText(friend.status_message)
@ -270,28 +278,33 @@ class Profile(basecontact.BaseContact, Singleton):
pixmap = QtGui.QPixmap(avatar_path)
self._screen.account_avatar.setPixmap(pixmap.scaled(64, 64, QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation))
self.update_filtration()
except Exception as ex: # no friend found. ignore
log('Friend value: ' + str(value))
log('Error in set active: ' + str(ex))
raise
def set_active_by_number(self, number):
for i in range(len(self._contacts)):
if self._contacts[i].number == number:
self._active_friend = i
break
active_friend = property(get_active, set_active)
def get_last_message(self):
if self._active_friend + 1:
return self._contacts[self._active_friend].get_last_message_text()
return self.get_curr_friend().get_last_message_text()
else:
return ''
def get_active_number(self):
return self._contacts[self._active_friend].number if self._active_friend + 1 else -1
return self.get_curr_friend().number if self._active_friend + 1 else -1
def get_active_name(self):
return self._contacts[self._active_friend].name if self._active_friend + 1 else ''
return self.get_curr_friend().name if self._active_friend + 1 else ''
def is_active_online(self):
return self._active_friend + 1 and self._contacts[self._active_friend].status is not None
return self._active_friend + 1 and self.get_curr_friend().status is not None
def new_name(self, number, name):
friend = self.get_friend_by_number(number)
@ -318,6 +331,7 @@ class Profile(basecontact.BaseContact, Singleton):
def send_files(self, friend_number):
friend = self.get_friend_by_number(friend_number)
friend.remove_invalid_unsent_files()
files = friend.get_unsent_files()
try:
for fl in files:
@ -366,7 +380,7 @@ class Profile(basecontact.BaseContact, Singleton):
"""
if Settings.get_instance()['typing_notifications'] and self._active_friend + 1:
try:
friend = self._contacts[self._active_friend]
friend = self.get_curr_friend()
if friend.status is not None:
self._tox.self_set_typing(friend.number, typing)
except:
@ -399,25 +413,25 @@ class Profile(basecontact.BaseContact, Singleton):
for message in messages:
self.split_and_send(friend_number, message.get_data()[-1], message.get_data()[0].encode('utf-8'))
friend.inc_receipts()
except:
pass
except Exception as ex:
log('Sending pending messages failed with ' + str(ex))
def split_and_send(self, number, message_type, message):
"""
Message splitting
Message splitting. Message length cannot be > TOX_MAX_MESSAGE_LENGTH
:param number: friend's number
:param message_type: type of message
:param message: message text
"""
while len(message) > TOX_MAX_MESSAGE_LENGTH:
size = TOX_MAX_MESSAGE_LENGTH * 4 / 5
size = TOX_MAX_MESSAGE_LENGTH * 4 // 5
last_part = message[size:TOX_MAX_MESSAGE_LENGTH]
if ' ' in last_part:
index = last_part.index(' ')
elif ',' in last_part:
index = last_part.index(',')
elif '.' in last_part:
index = last_part.index('.')
if b' ' in last_part:
index = last_part.index(b' ')
elif b',' in last_part:
index = last_part.index(b',')
elif b'.' in last_part:
index = last_part.index(b'.')
else:
index = TOX_MAX_MESSAGE_LENGTH - size - 1
index += size + 1
@ -436,7 +450,7 @@ class Profile(basecontact.BaseContact, Singleton):
t = time.time()
self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type)
self._messages.scrollToBottom()
self._contacts[self._active_friend].append_message(
self.get_curr_friend().append_message(
TextMessage(message, MESSAGE_OWNER['FRIEND'], t, message_type))
else:
friend = self.get_friend_by_number(friend_num)
@ -475,7 +489,7 @@ class Profile(basecontact.BaseContact, Singleton):
friend.append_message(TextMessage(text, MESSAGE_OWNER['NOT_SENT'], t, message_type))
def delete_message(self, time):
friend = self._contacts[self._active_friend]
friend = self.get_curr_friend()
friend.delete_message(time)
self._history.delete_message(friend.tox_id, time)
self.update()
@ -529,7 +543,7 @@ class Profile(basecontact.BaseContact, Singleton):
if not self._load_history:
return
self._load_history = False
friend = self._contacts[self._active_friend]
friend = self.get_curr_friend()
friend.load_corr(False)
data = friend.get_corr()
if not data:
@ -617,7 +631,7 @@ class Profile(basecontact.BaseContact, Singleton):
pixmap = None
if self._show_avatars:
if owner == MESSAGE_OWNER['FRIEND']:
pixmap = self._contacts[self._active_friend].get_pixmap()
pixmap = self.get_curr_friend().get_pixmap()
else:
pixmap = self.get_pixmap()
return self._factory.message_item(text, time, name, owner != MESSAGE_OWNER['NOT_SENT'],
@ -845,9 +859,11 @@ class Profile(basecontact.BaseContact, Singleton):
self.update_filtration()
def reconnect(self):
self._waiting_for_reconnection = False
if self.status is None or all(list(map(lambda x: x.status is None, self._contacts))) and len(self._contacts):
self._waiting_for_reconnection = True
self.reset(self._screen.reset)
QtCore.QTimer.singleShot(45000, self.reconnect)
QtCore.QTimer.singleShot(50000, self.reconnect)
def close(self):
for friend in self._contacts:
@ -876,7 +892,7 @@ class Profile(basecontact.BaseContact, Singleton):
settings = Settings.get_instance()
friend = self.get_friend_by_number(friend_number)
auto = settings['allow_auto_accept'] and friend.tox_id in settings['auto_accept_from_friends']
inline = (file_name in ALLOWED_FILES) and settings['allow_inline']
inline = is_inline(file_name) and settings['allow_inline']
file_id = self._tox.file_get_file_id(friend_number, file_number)
accepted = True
if file_id in self._paused_file_transfers:
@ -961,7 +977,7 @@ class Profile(basecontact.BaseContact, Singleton):
0, -1)
def cancel_not_started_transfer(self, time):
self._contacts[self._active_friend].delete_one_unsent_file(time)
self.get_curr_friend().delete_one_unsent_file(time)
self.update()
def pause_transfer(self, friend_number, file_number, by_friend=False):
@ -1144,7 +1160,6 @@ class Profile(basecontact.BaseContact, Singleton):
if not os.path.isfile(avatar_path): # reset image
avatar_path = None
sa = SendAvatar(avatar_path, self._tox, friend_number)
sa.set_transfer_finished_handler(self.transfer_finished)
self._file_transfers[(friend_number, sa.get_file_number())] = sa
def incoming_avatar(self, friend_number, file_number, size):
@ -1157,6 +1172,7 @@ class Profile(basecontact.BaseContact, Singleton):
ra = ReceiveAvatar(self._tox, friend_number, size, file_number)
if ra.state != TOX_FILE_TRANSFER_STATE['CANCELLED']:
self._file_transfers[(friend_number, file_number)] = ra
ra.set_transfer_finished_handler(self.transfer_finished)
else:
self.get_friend_by_number(friend_number).load_avatar()
if self.get_active_number() == friend_number:
@ -1195,7 +1211,7 @@ class Profile(basecontact.BaseContact, Singleton):
else:
text = QtGui.QApplication.translate("incoming_call", "Outgoing audio call", None,
QtGui.QApplication.UnicodeUTF8)
self._contacts[self._active_friend].append_message(InfoMessage(text, time.time()))
self.get_curr_friend().append_message(InfoMessage(text, time.time()))
self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE'])
self._messages.scrollToBottom()
elif num in self._call: # finish or cancel call if you call with active friend

View File

@ -3,7 +3,7 @@ import json
import os
from util import Singleton, curr_directory, log, copy, append_slash
import pyaudio
from toxencryptsave import ToxEncryptSave
from toxes import ToxES
import smileys
@ -19,7 +19,7 @@ class Settings(dict, Singleton):
if os.path.isfile(self.path):
with open(self.path, 'rb') as fl:
data = fl.read()
inst = ToxEncryptSave.get_instance()
inst = ToxES.get_instance()
try:
if inst.is_data_encrypted(data):
data = inst.pass_decrypt(data)
@ -35,6 +35,7 @@ class Settings(dict, Singleton):
smileys.SmileyLoader(self)
self.locked = False
self.closing = False
self.unlockScreen = False
p = pyaudio.PyAudio()
input_devices = output_devices = 0
for i in range(p.get_device_count()):
@ -49,7 +50,7 @@ class Settings(dict, Singleton):
@staticmethod
def get_auto_profile():
p = Settings.get_default_path() + 'toxygen.json'
p = Settings.get_global_settings_path()
if os.path.isfile(p):
with open(p) as fl:
data = fl.read()
@ -60,7 +61,7 @@ class Settings(dict, Singleton):
@staticmethod
def set_auto_profile(path, name):
p = Settings.get_default_path() + 'toxygen.json'
p = Settings.get_global_settings_path()
if os.path.isfile(p):
with open(p) as fl:
data = fl.read()
@ -74,7 +75,7 @@ class Settings(dict, Singleton):
@staticmethod
def reset_auto_profile():
p = Settings.get_default_path() + 'toxygen.json'
p = Settings.get_global_settings_path()
if os.path.isfile(p):
with open(p) as fl:
data = fl.read()
@ -89,15 +90,8 @@ class Settings(dict, Singleton):
@staticmethod
def is_active_profile(path, name):
path = path + name + '.tox'
settings = Settings.get_default_path() + 'toxygen.json'
if os.path.isfile(settings):
with open(settings) as fl:
data = fl.read()
data = json.loads(data)
if 'active_profile' in data:
return path in data['active_profile']
return False
path = path + name + '.lock'
return os.path.isfile(path)
@staticmethod
def get_default_settings():
@ -167,7 +161,7 @@ class Settings(dict, Singleton):
def save(self):
text = json.dumps(self)
inst = ToxEncryptSave.get_instance()
inst = ToxES.get_instance()
if inst.has_password():
text = bytes(inst.pass_encrypt(bytes(text, 'utf-8')))
else:
@ -176,37 +170,19 @@ class Settings(dict, Singleton):
fl.write(text)
def close(self):
path = Settings.get_default_path() + 'toxygen.json'
profile_path = ProfileHelper.get_path()
path = str(profile_path + str(self.name) + '.lock')
if os.path.isfile(path):
with open(path) as fl:
data = fl.read()
app_settings = json.loads(data)
try:
app_settings['active_profile'].remove(str(ProfileHelper.get_path() + self.name + '.tox'))
except:
pass
data = json.dumps(app_settings)
with open(path, 'w') as fl:
fl.write(data)
os.remove(path)
def set_active_profile(self):
"""
Mark current profile as active
"""
path = Settings.get_default_path() + 'toxygen.json'
if os.path.isfile(path):
with open(path) as fl:
data = fl.read()
app_settings = json.loads(data)
else:
app_settings = {}
if 'active_profile' not in app_settings:
app_settings['active_profile'] = []
profile_path = ProfileHelper.get_path()
app_settings['active_profile'].append(str(profile_path + str(self.name) + '.tox'))
data = json.dumps(app_settings)
path = str(profile_path + str(self.name) + '.lock')
with open(path, 'w') as fl:
fl.write(data)
fl.write('active')
def export(self, path):
text = json.dumps(self)
@ -216,6 +192,10 @@ class Settings(dict, Singleton):
def update_path(self):
self.path = ProfileHelper.get_path() + self.name + '.json'
@staticmethod
def get_global_settings_path():
return curr_directory() + '/toxygen.json'
@staticmethod
def get_default_path():
if system() == 'Windows':
@ -252,7 +232,7 @@ class ProfileHelper(Singleton):
return self._directory
def save_profile(self, data):
inst = ToxEncryptSave.get_instance()
inst = ToxES.get_instance()
if inst.has_password():
data = inst.pass_encrypt(data)
with open(self._path, 'wb') as fl:

View File

@ -1310,3 +1310,15 @@ QListWidget > QLabel
width: 0px;
height: 0px;
}
ClickableLabel:focus
{
border-width: 1px;
border-color: #4A4949;
border-style: solid;
}
ClickableLabel:hover
{
background-color: #4A4949;
}

View File

@ -1,64 +1,25 @@
import libtox
import util
from ctypes import c_size_t, create_string_buffer, byref, c_int, ArgumentError, c_char_p, c_bool
from toxencryptsave_enums_and_consts import *
TOX_ERR_ENCRYPTION = {
# The function returned successfully.
'OK': 0,
# Some input data, or maybe the output pointer, was null.
'NULL': 1,
# The crypto lib was unable to derive a key from the given passphrase, which is usually a lack of memory issue. The
# functions accepting keys do not produce this error.
'KEY_DERIVATION_FAILED': 2,
# The encryption itself failed.
'FAILED': 3
}
TOX_ERR_DECRYPTION = {
# The function returned successfully.
'OK': 0,
# Some input data, or maybe the output pointer, was null.
'NULL': 1,
# The input data was shorter than TOX_PASS_ENCRYPTION_EXTRA_LENGTH bytes
'INVALID_LENGTH': 2,
# The input data is missing the magic number (i.e. wasn't created by this module, or is corrupted)
'BAD_FORMAT': 3,
# The crypto lib was unable to derive a key from the given passphrase, which is usually a lack of memory issue. The
# functions accepting keys do not produce this error.
'KEY_DERIVATION_FAILED': 4,
# The encrypted byte array could not be decrypted. Either the data was corrupt or the password/key was incorrect.
'FAILED': 5,
}
TOX_PASS_ENCRYPTION_EXTRA_LENGTH = 80
class ToxEncryptSave(util.Singleton):
class ToxEncryptSave:
def __init__(self):
super().__init__()
self.libtoxencryptsave = libtox.LibToxEncryptSave()
self._passphrase = None
def set_password(self, passphrase):
self._passphrase = passphrase
def has_password(self):
return bool(self._passphrase)
def is_password(self, password):
return self._passphrase == password
def is_data_encrypted(self, data):
"""
Checks if given data is encrypted
"""
func = self.libtoxencryptsave.tox_is_data_encrypted
func.restype = c_bool
result = func(c_char_p(bytes(data)))
return result
def pass_encrypt(self, data):
def pass_encrypt(self, data, password):
"""
Encrypts the given data with the given passphrase.
Encrypts the given data with the given password.
:return: output array
"""
@ -66,8 +27,8 @@ class ToxEncryptSave(util.Singleton):
tox_err_encryption = c_int()
self.libtoxencryptsave.tox_pass_encrypt(c_char_p(data),
c_size_t(len(data)),
c_char_p(bytes(self._passphrase, 'utf-8')),
c_size_t(len(self._passphrase)),
c_char_p(bytes(password, 'utf-8')),
c_size_t(len(password)),
out,
byref(tox_err_encryption))
tox_err_encryption = tox_err_encryption.value
@ -81,9 +42,9 @@ class ToxEncryptSave(util.Singleton):
elif tox_err_encryption == TOX_ERR_ENCRYPTION['FAILED']:
raise RuntimeError('The encryption itself failed.')
def pass_decrypt(self, data):
def pass_decrypt(self, data, password):
"""
Decrypts the given data with the given passphrase.
Decrypts the given data with the given password.
:return: output array
"""
@ -91,8 +52,8 @@ class ToxEncryptSave(util.Singleton):
tox_err_decryption = c_int()
self.libtoxencryptsave.tox_pass_decrypt(c_char_p(bytes(data)),
c_size_t(len(data)),
c_char_p(bytes(self._passphrase, 'utf-8')),
c_size_t(len(self._passphrase)),
c_char_p(bytes(password, 'utf-8')),
c_size_t(len(password)),
out,
byref(tox_err_decryption))
tox_err_decryption = tox_err_decryption.value

View File

@ -0,0 +1,29 @@
TOX_ERR_ENCRYPTION = {
# The function returned successfully.
'OK': 0,
# Some input data, or maybe the output pointer, was null.
'NULL': 1,
# The crypto lib was unable to derive a key from the given passphrase, which is usually a lack of memory issue. The
# functions accepting keys do not produce this error.
'KEY_DERIVATION_FAILED': 2,
# The encryption itself failed.
'FAILED': 3
}
TOX_ERR_DECRYPTION = {
# The function returned successfully.
'OK': 0,
# Some input data, or maybe the output pointer, was null.
'NULL': 1,
# The input data was shorter than TOX_PASS_ENCRYPTION_EXTRA_LENGTH bytes
'INVALID_LENGTH': 2,
# The input data is missing the magic number (i.e. wasn't created by this module, or is corrupted)
'BAD_FORMAT': 3,
# The crypto lib was unable to derive a key from the given passphrase, which is usually a lack of memory issue. The
# functions accepting keys do not produce this error.
'KEY_DERIVATION_FAILED': 4,
# The encrypted byte array could not be decrypted. Either the data was corrupt or the password/key was incorrect.
'FAILED': 5,
}
TOX_PASS_ENCRYPTION_EXTRA_LENGTH = 80

28
toxygen/toxes.py Normal file
View File

@ -0,0 +1,28 @@
import util
import toxencryptsave
class ToxES(util.Singleton):
def __init__(self):
super().__init__()
self._toxencryptsave = toxencryptsave.ToxEncryptSave()
self._passphrase = None
def set_password(self, passphrase):
self._passphrase = passphrase
def has_password(self):
return bool(self._passphrase)
def is_password(self, password):
return self._passphrase == password
def is_data_encrypted(self, data):
return len(data) > 0 and self._toxencryptsave.is_data_encrypted(data)
def pass_encrypt(self, data):
return self._toxencryptsave.pass_encrypt(data, self._passphrase)
def pass_decrypt(self, data):
return self._toxencryptsave.pass_decrypt(data, self._passphrase)

View File

@ -84,98 +84,98 @@ can produce IP leak</source>
<context>
<name>MainWindow</name>
<message>
<location filename="mainscreen.py" line="118"/>
<location filename="mainscreen.py" line="123"/>
<source>Profile</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="124"/>
<location filename="mainscreen.py" line="129"/>
<source>Settings</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="414"/>
<location filename="mainscreen.py" line="426"/>
<source>About</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="117"/>
<location filename="mainscreen.py" line="122"/>
<source>Add contact</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="119"/>
<location filename="mainscreen.py" line="124"/>
<source>Privacy</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="120"/>
<location filename="mainscreen.py" line="125"/>
<source>Interface</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="121"/>
<location filename="mainscreen.py" line="126"/>
<source>Notifications</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="122"/>
<location filename="mainscreen.py" line="127"/>
<source>Network</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="123"/>
<location filename="mainscreen.py" line="128"/>
<source>About program</source>
<translation></translation>
</message>
<message>
<location filename="profile.py" line="816"/>
<location filename="profile.py" line="829"/>
<source>User {} wants to add you to contact list. Message:
{}</source>
<translation></translation>
</message>
<message>
<location filename="profile.py" line="818"/>
<location filename="profile.py" line="831"/>
<source>Friend request</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="509"/>
<location filename="mainscreen.py" line="528"/>
<source>Choose file</source>
<translation>Choose file</translation>
</message>
<message>
<location filename="mainscreen.py" line="574"/>
<location filename="mainscreen.py" line="593"/>
<source>Disallow auto accept</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="575"/>
<location filename="mainscreen.py" line="594"/>
<source>Allow auto accept</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="577"/>
<location filename="mainscreen.py" line="596"/>
<source>Set alias</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="580"/>
<location filename="mainscreen.py" line="599"/>
<source>Clear history</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="590"/>
<location filename="mainscreen.py" line="609"/>
<source>Remove friend</source>
<translation></translation>
</message>
<message>
<location filename="profile.py" line="655"/>
<location filename="profile.py" line="668"/>
<source>Enter new alias for friend {} or leave empty to use friend&apos;s name:</source>
<translation>Enter new alias for friend {} or leave empty to use friend&apos;s name:</translation>
</message>
<message>
<location filename="mainscreen.py" line="125"/>
<location filename="mainscreen.py" line="130"/>
<source>Audio</source>
<translation>Audio</translation>
</message>
@ -185,24 +185,24 @@ can produce IP leak</source>
<translation type="obsolete">Find contact</translation>
</message>
<message>
<location filename="profile.py" line="788"/>
<location filename="profile.py" line="801"/>
<source>Friend added</source>
<translation>Friend added</translation>
</message>
<message>
<location filename="mainscreen.py" line="415"/>
<location filename="mainscreen.py" line="427"/>
<source>Toxygen is Tox client written on Python.
Version: </source>
<translation>Toxygen is Tox client written on Python.
Version:</translation>
</message>
<message>
<location filename="profile.py" line="789"/>
<location filename="profile.py" line="802"/>
<source>Friend added without sending friend request</source>
<translation>Friend added without sending friend request</translation>
</message>
<message>
<location filename="mainscreen.py" line="632"/>
<location filename="mainscreen.py" line="655"/>
<source>Choose folder</source>
<translation>Choose folder</translation>
</message>
@ -217,47 +217,47 @@ Version:</translation>
<translation type="obsolete">Send file</translation>
</message>
<message>
<location filename="mainscreen.py" line="128"/>
<location filename="mainscreen.py" line="133"/>
<source>Send message</source>
<translation>Send message</translation>
</message>
<message>
<location filename="mainscreen.py" line="129"/>
<location filename="mainscreen.py" line="134"/>
<source>Start audio call with friend</source>
<translation>Start audio call with friend</translation>
</message>
<message>
<location filename="mainscreen.py" line="595"/>
<location filename="mainscreen.py" line="617"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="113"/>
<location filename="mainscreen.py" line="118"/>
<source>List of plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="127"/>
<location filename="mainscreen_widgets.py" line="462"/>
<source>Search</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="131"/>
<location filename="mainscreen.py" line="136"/>
<source>All</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="132"/>
<location filename="mainscreen.py" line="137"/>
<source>Online</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="591"/>
<location filename="mainscreen.py" line="611"/>
<source>Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="616"/>
<location filename="mainscreen.py" line="639"/>
<source>Notes about user</source>
<translation type="unfinished"></translation>
</message>
@ -307,42 +307,42 @@ Version:</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="profile.py" line="303"/>
<location filename="profile.py" line="315"/>
<source>User {} is now known as {}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="list_items.py" line="167"/>
<location filename="list_items.py" line="168"/>
<source>Delete message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="111"/>
<location filename="mainscreen.py" line="116"/>
<source>Lock</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="482"/>
<location filename="mainscreen.py" line="501"/>
<source>Cannot lock app</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="485"/>
<location filename="mainscreen.py" line="504"/>
<source>Error. Profile password is not set.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="585"/>
<location filename="mainscreen.py" line="604"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="586"/>
<location filename="mainscreen.py" line="605"/>
<source>Status message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="587"/>
<location filename="mainscreen.py" line="606"/>
<source>Public key</source>
<translation type="unfinished"></translation>
</message>
@ -367,65 +367,85 @@ Version:</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="140"/>
<location filename="mainscreen.py" line="145"/>
<source>Import plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="460"/>
<location filename="mainscreen.py" line="479"/>
<source>Choose folder with plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="469"/>
<location filename="mainscreen.py" line="488"/>
<source>Restart Toxygen</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="472"/>
<location filename="mainscreen.py" line="491"/>
<source>Plugin will be loaded after restart</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="list_items.py" line="48"/>
<location filename="list_items.py" line="49"/>
<source>Quote selected text</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="579"/>
<location filename="mainscreen.py" line="598"/>
<source>Chat history</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="581"/>
<location filename="mainscreen.py" line="600"/>
<source>Export as text</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="582"/>
<location filename="mainscreen.py" line="601"/>
<source>Export as HTML</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="126"/>
<location filename="mainscreen.py" line="131"/>
<source>Updates</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="133"/>
<location filename="mainscreen.py" line="138"/>
<source>Online first</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="135"/>
<location filename="mainscreen.py" line="140"/>
<source>Online and by name</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="136"/>
<location filename="mainscreen.py" line="141"/>
<source>Online first and by name</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="610"/>
<source>Block friend</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="528"/>
<source>Not found</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="524"/>
<source>Text &quot;{}&quot; was not found</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="146"/>
<source>Reload plugins</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MenuWindow</name>
@ -758,30 +778,25 @@ Version:</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="374"/>
<location filename="mainscreen_widgets.py" line="370"/>
<source>Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="386"/>
<location filename="mainscreen_widgets.py" line="382"/>
<source>Set new NoSpam to avoid spam friend requests: Profile -&gt; Settings -&gt; Set new NoSpam.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="378"/>
<location filename="mainscreen_widgets.py" line="374"/>
<source>Delete single message in chat: make right click on spinner or message time and choose &quot;Delete&quot; in menu</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="382"/>
<location filename="mainscreen_widgets.py" line="378"/>
<source>Use right click on inline image to save it</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="370"/>
<source>New in Toxygen v0.2.6:&lt;br&gt;Updater&lt;br&gt;Better contact sorting&lt;br&gt;Plugins improvements</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>audioSettingsForm</name>
@ -804,32 +819,32 @@ Version:</translation>
<context>
<name>incoming_call</name>
<message>
<location filename="profile.py" line="1214"/>
<location filename="profile.py" line="1229"/>
<source>Incoming video call</source>
<translation>Incoming video call</translation>
</message>
<message>
<location filename="profile.py" line="1217"/>
<location filename="profile.py" line="1232"/>
<source>Incoming audio call</source>
<translation>Incoming audio call</translation>
</message>
<message>
<location filename="profile.py" line="1195"/>
<location filename="profile.py" line="1210"/>
<source>Outgoing video call</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="profile.py" line="1198"/>
<location filename="profile.py" line="1213"/>
<source>Outgoing audio call</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="profile.py" line="1248"/>
<location filename="profile.py" line="1263"/>
<source>Call declined</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="profile.py" line="1250"/>
<location filename="profile.py" line="1265"/>
<source>Call finished</source>
<translation type="unfinished"></translation>
</message>
@ -985,7 +1000,7 @@ Version:</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="288"/>
<location filename="main.py" line="290"/>
<source>Update for Toxygen was found. Download and install it?</source>
<translation type="unfinished"></translation>
</message>
@ -1122,32 +1137,32 @@ Version:</translation>
<context>
<name>tray</name>
<message>
<location filename="main.py" line="228"/>
<location filename="main.py" line="229"/>
<source>Open Toxygen</source>
<translation></translation>
</message>
<message>
<location filename="main.py" line="237"/>
<location filename="main.py" line="238"/>
<source>Exit</source>
<translation></translation>
</message>
<message>
<location filename="main.py" line="229"/>
<location filename="main.py" line="230"/>
<source>Set status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="230"/>
<location filename="main.py" line="231"/>
<source>Online</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="231"/>
<location filename="main.py" line="232"/>
<source>Away</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="232"/>
<location filename="main.py" line="233"/>
<source>Busy</source>
<translation type="unfinished"></translation>
</message>

View File

@ -84,58 +84,58 @@ can produce IP leak</source>
<context>
<name>MainWindow</name>
<message>
<location filename="mainscreen.py" line="118"/>
<location filename="mainscreen.py" line="123"/>
<source>Profile</source>
<translation>Profile</translation>
</message>
<message>
<location filename="mainscreen.py" line="124"/>
<location filename="mainscreen.py" line="129"/>
<source>Settings</source>
<translation>Paramêtres</translation>
</message>
<message>
<location filename="mainscreen.py" line="414"/>
<location filename="mainscreen.py" line="426"/>
<source>About</source>
<translation>À Propos</translation>
</message>
<message>
<location filename="mainscreen.py" line="117"/>
<location filename="mainscreen.py" line="122"/>
<source>Add contact</source>
<translation>Rajouter un contact</translation>
</message>
<message>
<location filename="mainscreen.py" line="119"/>
<location filename="mainscreen.py" line="124"/>
<source>Privacy</source>
<translation>Confidentialité</translation>
</message>
<message>
<location filename="mainscreen.py" line="120"/>
<location filename="mainscreen.py" line="125"/>
<source>Interface</source>
<translation>Interface</translation>
</message>
<message>
<location filename="mainscreen.py" line="121"/>
<location filename="mainscreen.py" line="126"/>
<source>Notifications</source>
<translation>Notifications</translation>
</message>
<message>
<location filename="mainscreen.py" line="122"/>
<location filename="mainscreen.py" line="127"/>
<source>Network</source>
<translation>Réseau</translation>
</message>
<message>
<location filename="mainscreen.py" line="123"/>
<location filename="mainscreen.py" line="128"/>
<source>About program</source>
<translation>À propos du programme</translation>
</message>
<message>
<location filename="profile.py" line="816"/>
<location filename="profile.py" line="829"/>
<source>User {} wants to add you to contact list. Message:
{}</source>
<translation>L&apos;Utilisateur {} veut vout rajouter à sa liste de contacts. Message : {}</translation>
</message>
<message>
<location filename="profile.py" line="818"/>
<location filename="profile.py" line="831"/>
<source>Friend request</source>
<translation>Demande d&apos;amis</translation>
</message>
@ -145,27 +145,27 @@ can produce IP leak</source>
<translation type="obsolete">Toxygen est un client Tox écris en Python 2.7. Version : </translation>
</message>
<message>
<location filename="mainscreen.py" line="509"/>
<location filename="mainscreen.py" line="528"/>
<source>Choose file</source>
<translation>Choisir un fichier</translation>
</message>
<message>
<location filename="mainscreen.py" line="574"/>
<location filename="mainscreen.py" line="593"/>
<source>Disallow auto accept</source>
<translation>Désactiver l&apos;auto-réception</translation>
</message>
<message>
<location filename="mainscreen.py" line="575"/>
<location filename="mainscreen.py" line="594"/>
<source>Allow auto accept</source>
<translation>Activer l&apos;auto-réception</translation>
</message>
<message>
<location filename="mainscreen.py" line="577"/>
<location filename="mainscreen.py" line="596"/>
<source>Set alias</source>
<translation>Définir un alias</translation>
</message>
<message>
<location filename="mainscreen.py" line="580"/>
<location filename="mainscreen.py" line="599"/>
<source>Clear history</source>
<translation>Vider l&apos;historique</translation>
</message>
@ -175,17 +175,17 @@ can produce IP leak</source>
<translation type="obsolete">Copier la clé publique</translation>
</message>
<message>
<location filename="mainscreen.py" line="590"/>
<location filename="mainscreen.py" line="609"/>
<source>Remove friend</source>
<translation>Retirer un ami</translation>
</message>
<message>
<location filename="profile.py" line="655"/>
<location filename="profile.py" line="668"/>
<source>Enter new alias for friend {} or leave empty to use friend&apos;s name:</source>
<translation>Entrez un nouvel alias pour l&apos;ami {} ou laissez vide pour garder son nom de base :</translation>
</message>
<message>
<location filename="mainscreen.py" line="125"/>
<location filename="mainscreen.py" line="130"/>
<source>Audio</source>
<translation>Audio</translation>
</message>
@ -195,24 +195,24 @@ can produce IP leak</source>
<translation type="obsolete">Trouver le contact</translation>
</message>
<message>
<location filename="profile.py" line="788"/>
<location filename="profile.py" line="801"/>
<source>Friend added</source>
<translation>Ami rajouté</translation>
</message>
<message>
<location filename="mainscreen.py" line="415"/>
<location filename="mainscreen.py" line="427"/>
<source>Toxygen is Tox client written on Python.
Version: </source>
<translation>Toxygen est un client Tox écrit en Python.
Version :</translation>
</message>
<message>
<location filename="profile.py" line="789"/>
<location filename="profile.py" line="802"/>
<source>Friend added without sending friend request</source>
<translation>Ami rajouté sans avoir envoyé de demande</translation>
</message>
<message>
<location filename="mainscreen.py" line="632"/>
<location filename="mainscreen.py" line="655"/>
<source>Choose folder</source>
<translation>Choisir le dossier</translation>
</message>
@ -227,47 +227,47 @@ Version :</translation>
<translation type="obsolete">Envoyer le fichier</translation>
</message>
<message>
<location filename="mainscreen.py" line="128"/>
<location filename="mainscreen.py" line="133"/>
<source>Send message</source>
<translation>Envoyer le message</translation>
</message>
<message>
<location filename="mainscreen.py" line="129"/>
<location filename="mainscreen.py" line="134"/>
<source>Start audio call with friend</source>
<translation>Lancer un appel audio avec un ami</translation>
</message>
<message>
<location filename="mainscreen.py" line="595"/>
<location filename="mainscreen.py" line="617"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="113"/>
<location filename="mainscreen.py" line="118"/>
<source>List of plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="127"/>
<location filename="mainscreen_widgets.py" line="462"/>
<source>Search</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="131"/>
<location filename="mainscreen.py" line="136"/>
<source>All</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="132"/>
<location filename="mainscreen.py" line="137"/>
<source>Online</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="591"/>
<location filename="mainscreen.py" line="611"/>
<source>Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="616"/>
<location filename="mainscreen.py" line="639"/>
<source>Notes about user</source>
<translation type="unfinished"></translation>
</message>
@ -317,42 +317,42 @@ Version :</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="profile.py" line="303"/>
<location filename="profile.py" line="315"/>
<source>User {} is now known as {}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="list_items.py" line="167"/>
<location filename="list_items.py" line="168"/>
<source>Delete message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="111"/>
<location filename="mainscreen.py" line="116"/>
<source>Lock</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="482"/>
<location filename="mainscreen.py" line="501"/>
<source>Cannot lock app</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="485"/>
<location filename="mainscreen.py" line="504"/>
<source>Error. Profile password is not set.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="585"/>
<location filename="mainscreen.py" line="604"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="586"/>
<location filename="mainscreen.py" line="605"/>
<source>Status message</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="587"/>
<location filename="mainscreen.py" line="606"/>
<source>Public key</source>
<translation type="unfinished"></translation>
</message>
@ -377,65 +377,85 @@ Version :</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="140"/>
<location filename="mainscreen.py" line="145"/>
<source>Import plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="460"/>
<location filename="mainscreen.py" line="479"/>
<source>Choose folder with plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="469"/>
<location filename="mainscreen.py" line="488"/>
<source>Restart Toxygen</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="472"/>
<location filename="mainscreen.py" line="491"/>
<source>Plugin will be loaded after restart</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="list_items.py" line="48"/>
<location filename="list_items.py" line="49"/>
<source>Quote selected text</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="579"/>
<location filename="mainscreen.py" line="598"/>
<source>Chat history</source>
<translation type="unfinished">Historique de chat</translation>
</message>
<message>
<location filename="mainscreen.py" line="581"/>
<location filename="mainscreen.py" line="600"/>
<source>Export as text</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="582"/>
<location filename="mainscreen.py" line="601"/>
<source>Export as HTML</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="126"/>
<location filename="mainscreen.py" line="131"/>
<source>Updates</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="133"/>
<location filename="mainscreen.py" line="138"/>
<source>Online first</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="135"/>
<location filename="mainscreen.py" line="140"/>
<source>Online and by name</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="136"/>
<location filename="mainscreen.py" line="141"/>
<source>Online first and by name</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="610"/>
<source>Block friend</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="528"/>
<source>Not found</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="524"/>
<source>Text &quot;{}&quot; was not found</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="146"/>
<source>Reload plugins</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MenuWindow</name>
@ -768,30 +788,25 @@ Version :</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="374"/>
<location filename="mainscreen_widgets.py" line="370"/>
<source>Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="386"/>
<location filename="mainscreen_widgets.py" line="382"/>
<source>Set new NoSpam to avoid spam friend requests: Profile -&gt; Settings -&gt; Set new NoSpam.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="378"/>
<location filename="mainscreen_widgets.py" line="374"/>
<source>Delete single message in chat: make right click on spinner or message time and choose &quot;Delete&quot; in menu</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="382"/>
<location filename="mainscreen_widgets.py" line="378"/>
<source>Use right click on inline image to save it</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="370"/>
<source>New in Toxygen v0.2.6:&lt;br&gt;Updater&lt;br&gt;Better contact sorting&lt;br&gt;Plugins improvements</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>audioSettingsForm</name>
@ -814,32 +829,32 @@ Version :</translation>
<context>
<name>incoming_call</name>
<message>
<location filename="profile.py" line="1214"/>
<location filename="profile.py" line="1229"/>
<source>Incoming video call</source>
<translation>Appel vidéo entrant</translation>
</message>
<message>
<location filename="profile.py" line="1217"/>
<location filename="profile.py" line="1232"/>
<source>Incoming audio call</source>
<translation>Appel audio entrant</translation>
</message>
<message>
<location filename="profile.py" line="1195"/>
<location filename="profile.py" line="1210"/>
<source>Outgoing video call</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="profile.py" line="1198"/>
<location filename="profile.py" line="1213"/>
<source>Outgoing audio call</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="profile.py" line="1248"/>
<location filename="profile.py" line="1263"/>
<source>Call declined</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="profile.py" line="1250"/>
<location filename="profile.py" line="1265"/>
<source>Call finished</source>
<translation type="unfinished"></translation>
</message>
@ -1000,7 +1015,7 @@ Version :</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="288"/>
<location filename="main.py" line="290"/>
<source>Update for Toxygen was found. Download and install it?</source>
<translation type="unfinished"></translation>
</message>
@ -1137,32 +1152,32 @@ Version :</translation>
<context>
<name>tray</name>
<message>
<location filename="main.py" line="228"/>
<location filename="main.py" line="229"/>
<source>Open Toxygen</source>
<translation>Ouvrir Toxygen</translation>
</message>
<message>
<location filename="main.py" line="237"/>
<location filename="main.py" line="238"/>
<source>Exit</source>
<translation>Quitter</translation>
</message>
<message>
<location filename="main.py" line="229"/>
<location filename="main.py" line="230"/>
<source>Set status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="230"/>
<location filename="main.py" line="231"/>
<source>Online</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="231"/>
<location filename="main.py" line="232"/>
<source>Away</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="232"/>
<location filename="main.py" line="233"/>
<source>Busy</source>
<translation type="unfinished"></translation>
</message>

Binary file not shown.

View File

@ -87,84 +87,84 @@ can produce IP leak</source>
<context>
<name>MainWindow</name>
<message>
<location filename="mainscreen.py" line="118"/>
<location filename="mainscreen.py" line="123"/>
<source>Profile</source>
<translation>Профиль</translation>
</message>
<message>
<location filename="mainscreen.py" line="124"/>
<location filename="mainscreen.py" line="129"/>
<source>Settings</source>
<translation>Настройки</translation>
</message>
<message>
<location filename="mainscreen.py" line="414"/>
<location filename="mainscreen.py" line="426"/>
<source>About</source>
<translation>О программе</translation>
</message>
<message>
<location filename="mainscreen.py" line="117"/>
<location filename="mainscreen.py" line="122"/>
<source>Add contact</source>
<translation>Добавить контакт</translation>
</message>
<message>
<location filename="mainscreen.py" line="119"/>
<location filename="mainscreen.py" line="124"/>
<source>Privacy</source>
<translation>Приватность</translation>
</message>
<message>
<location filename="mainscreen.py" line="120"/>
<location filename="mainscreen.py" line="125"/>
<source>Interface</source>
<translation>Интерфейс</translation>
</message>
<message>
<location filename="mainscreen.py" line="121"/>
<location filename="mainscreen.py" line="126"/>
<source>Notifications</source>
<translation>Уведомления</translation>
</message>
<message>
<location filename="mainscreen.py" line="122"/>
<location filename="mainscreen.py" line="127"/>
<source>Network</source>
<translation>Сеть</translation>
</message>
<message>
<location filename="mainscreen.py" line="123"/>
<location filename="mainscreen.py" line="128"/>
<source>About program</source>
<translation>О программе</translation>
</message>
<message>
<location filename="profile.py" line="816"/>
<location filename="profile.py" line="829"/>
<source>User {} wants to add you to contact list. Message:
{}</source>
<translation>Пользователь {} хочет добавить Вас в список контактов. Сообщение:
{}</translation>
</message>
<message>
<location filename="profile.py" line="818"/>
<location filename="profile.py" line="831"/>
<source>Friend request</source>
<translation>Запрос на добавление в друзья</translation>
</message>
<message>
<location filename="mainscreen.py" line="509"/>
<location filename="mainscreen.py" line="528"/>
<source>Choose file</source>
<translation>Выберите файл</translation>
</message>
<message>
<location filename="mainscreen.py" line="574"/>
<location filename="mainscreen.py" line="593"/>
<source>Disallow auto accept</source>
<translation>Запретить автоматическое получение файлов</translation>
</message>
<message>
<location filename="mainscreen.py" line="575"/>
<location filename="mainscreen.py" line="594"/>
<source>Allow auto accept</source>
<translation>Разрешить автоматическое получение файлов</translation>
</message>
<message>
<location filename="mainscreen.py" line="577"/>
<location filename="mainscreen.py" line="596"/>
<source>Set alias</source>
<translation>Изменить псевдоним</translation>
</message>
<message>
<location filename="mainscreen.py" line="580"/>
<location filename="mainscreen.py" line="599"/>
<source>Clear history</source>
<translation>Очистить историю</translation>
</message>
@ -174,17 +174,17 @@ can produce IP leak</source>
<translation type="obsolete">Копировать публичный ключ</translation>
</message>
<message>
<location filename="mainscreen.py" line="590"/>
<location filename="mainscreen.py" line="609"/>
<source>Remove friend</source>
<translation>Удалить друга</translation>
</message>
<message>
<location filename="profile.py" line="655"/>
<location filename="profile.py" line="668"/>
<source>Enter new alias for friend {} or leave empty to use friend&apos;s name:</source>
<translation>Введите новый псевдоним для друга {} или оставьте пустым для использования его имени:</translation>
</message>
<message>
<location filename="mainscreen.py" line="125"/>
<location filename="mainscreen.py" line="130"/>
<source>Audio</source>
<translation>Аудио</translation>
</message>
@ -194,23 +194,23 @@ can produce IP leak</source>
<translation type="obsolete">Найти контакт</translation>
</message>
<message>
<location filename="profile.py" line="788"/>
<location filename="profile.py" line="801"/>
<source>Friend added</source>
<translation>Друг добавлен</translation>
</message>
<message>
<location filename="mainscreen.py" line="415"/>
<location filename="mainscreen.py" line="427"/>
<source>Toxygen is Tox client written on Python.
Version: </source>
<translation>Toxygen - клиент для мессенджера Tox, написанный на Python. Версия: </translation>
</message>
<message>
<location filename="profile.py" line="789"/>
<location filename="profile.py" line="802"/>
<source>Friend added without sending friend request</source>
<translation>Друг добавлен без отправки запроса на добавление в друзья</translation>
</message>
<message>
<location filename="mainscreen.py" line="632"/>
<location filename="mainscreen.py" line="655"/>
<source>Choose folder</source>
<translation>Выбрать папку</translation>
</message>
@ -225,47 +225,47 @@ Version: </source>
<translation type="obsolete">Отправить файл</translation>
</message>
<message>
<location filename="mainscreen.py" line="128"/>
<location filename="mainscreen.py" line="133"/>
<source>Send message</source>
<translation>Отправить сообщение</translation>
</message>
<message>
<location filename="mainscreen.py" line="129"/>
<location filename="mainscreen.py" line="134"/>
<source>Start audio call with friend</source>
<translation>Начать аудиозвонок с другом</translation>
</message>
<message>
<location filename="mainscreen.py" line="595"/>
<location filename="mainscreen.py" line="617"/>
<source>Plugins</source>
<translation>Плагины</translation>
</message>
<message>
<location filename="mainscreen.py" line="113"/>
<location filename="mainscreen.py" line="118"/>
<source>List of plugins</source>
<translation>Список плагинов</translation>
</message>
<message>
<location filename="mainscreen.py" line="127"/>
<location filename="mainscreen_widgets.py" line="462"/>
<source>Search</source>
<translation>Поиск</translation>
</message>
<message>
<location filename="mainscreen.py" line="131"/>
<location filename="mainscreen.py" line="136"/>
<source>All</source>
<translation>Все</translation>
</message>
<message>
<location filename="mainscreen.py" line="132"/>
<location filename="mainscreen.py" line="137"/>
<source>Online</source>
<translation>Онлайн</translation>
</message>
<message>
<location filename="mainscreen.py" line="591"/>
<location filename="mainscreen.py" line="611"/>
<source>Notes</source>
<translation>Заметки</translation>
</message>
<message>
<location filename="mainscreen.py" line="616"/>
<location filename="mainscreen.py" line="639"/>
<source>Notes about user</source>
<translation>Заметки о пользователе</translation>
</message>
@ -315,42 +315,42 @@ Version: </source>
<translation>Сохранить</translation>
</message>
<message>
<location filename="profile.py" line="303"/>
<location filename="profile.py" line="315"/>
<source>User {} is now known as {}</source>
<translation>Пользователь {} сейчас известен как {}</translation>
</message>
<message>
<location filename="list_items.py" line="167"/>
<location filename="list_items.py" line="168"/>
<source>Delete message</source>
<translation>Удалить сообщение</translation>
</message>
<message>
<location filename="mainscreen.py" line="111"/>
<location filename="mainscreen.py" line="116"/>
<source>Lock</source>
<translation>Заблокировать</translation>
</message>
<message>
<location filename="mainscreen.py" line="482"/>
<location filename="mainscreen.py" line="501"/>
<source>Cannot lock app</source>
<translation>Невозможно заблокировать приложение</translation>
</message>
<message>
<location filename="mainscreen.py" line="485"/>
<location filename="mainscreen.py" line="504"/>
<source>Error. Profile password is not set.</source>
<translation>Ошибка. Пароль профиля не установлен.</translation>
</message>
<message>
<location filename="mainscreen.py" line="585"/>
<location filename="mainscreen.py" line="604"/>
<source>Name</source>
<translation>Имя</translation>
</message>
<message>
<location filename="mainscreen.py" line="586"/>
<location filename="mainscreen.py" line="605"/>
<source>Status message</source>
<translation>Статус</translation>
</message>
<message>
<location filename="mainscreen.py" line="587"/>
<location filename="mainscreen.py" line="606"/>
<source>Public key</source>
<translation>Публичный ключ</translation>
</message>
@ -375,65 +375,85 @@ Version: </source>
<translation>Выберите папку с паком смайлов</translation>
</message>
<message>
<location filename="mainscreen.py" line="140"/>
<location filename="mainscreen.py" line="145"/>
<source>Import plugin</source>
<translation>Импортировать плагин</translation>
</message>
<message>
<location filename="mainscreen.py" line="460"/>
<location filename="mainscreen.py" line="479"/>
<source>Choose folder with plugin</source>
<translation>Выберите папку с плагином</translation>
</message>
<message>
<location filename="mainscreen.py" line="469"/>
<location filename="mainscreen.py" line="488"/>
<source>Restart Toxygen</source>
<translation>Перезапустите Toxygen</translation>
</message>
<message>
<location filename="mainscreen.py" line="472"/>
<location filename="mainscreen.py" line="491"/>
<source>Plugin will be loaded after restart</source>
<translation>Плагин будет загружен после перезапуска</translation>
</message>
<message>
<location filename="list_items.py" line="48"/>
<location filename="list_items.py" line="49"/>
<source>Quote selected text</source>
<translation>Цитировать выбранный текст</translation>
</message>
<message>
<location filename="mainscreen.py" line="579"/>
<location filename="mainscreen.py" line="598"/>
<source>Chat history</source>
<translation>История чата</translation>
</message>
<message>
<location filename="mainscreen.py" line="581"/>
<location filename="mainscreen.py" line="600"/>
<source>Export as text</source>
<translation>Экспортировать как текст</translation>
</message>
<message>
<location filename="mainscreen.py" line="582"/>
<location filename="mainscreen.py" line="601"/>
<source>Export as HTML</source>
<translation>Экспортировать как HTML</translation>
</message>
<message>
<location filename="mainscreen.py" line="126"/>
<location filename="mainscreen.py" line="131"/>
<source>Updates</source>
<translation>Обновления</translation>
</message>
<message>
<location filename="mainscreen.py" line="133"/>
<location filename="mainscreen.py" line="138"/>
<source>Online first</source>
<translation>Сначала онлайн</translation>
</message>
<message>
<location filename="mainscreen.py" line="135"/>
<location filename="mainscreen.py" line="140"/>
<source>Online and by name</source>
<translation>Онлайн и по имени</translation>
</message>
<message>
<location filename="mainscreen.py" line="136"/>
<location filename="mainscreen.py" line="141"/>
<source>Online first and by name</source>
<translation>Сначала онлайн и по имени</translation>
</message>
<message>
<location filename="mainscreen.py" line="610"/>
<source>Block friend</source>
<translation>Заблокировать друга</translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="528"/>
<source>Not found</source>
<translation>Не найдено</translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="524"/>
<source>Text &quot;{}&quot; was not found</source>
<translation>Текст &quot;{}&quot; не был найден</translation>
</message>
<message>
<location filename="mainscreen.py" line="146"/>
<source>Reload plugins</source>
<translation>Перезагрузить плагины</translation>
</message>
</context>
<context>
<name>MenuWindow</name>
@ -796,12 +816,12 @@ Version: </source>
<translation>Используйте Настройки -&gt; Интерфейс для настройки интерфейса.</translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="374"/>
<location filename="mainscreen_widgets.py" line="370"/>
<source>Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.</source>
<translation>Toxygen поддерживает псевдооффлайн сообщения и файл трансферы.</translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="386"/>
<location filename="mainscreen_widgets.py" line="382"/>
<source>Set new NoSpam to avoid spam friend requests: Profile -&gt; Settings -&gt; Set new NoSpam.</source>
<translation>Установите новый NoSpam, чтобы избежать спам запросов в друзья: Профиль-&gt;Настройки-&gt;Новый NoSpam.</translation>
</message>
@ -811,12 +831,12 @@ Version: </source>
<translation type="obsolete">Новое в Toxygen 0.2.3:&lt;br&gt;Соответствие TCS&lt;br&gt;Импорт плагинов, смайлов и стикеров&lt;br&gt;Исправления ошибок</translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="378"/>
<location filename="mainscreen_widgets.py" line="374"/>
<source>Delete single message in chat: make right click on spinner or message time and choose &quot;Delete&quot; in menu</source>
<translation>Чтобы удалить отдельное сообщение в чате сделайте правый клик на спиннер или время сообщения и выберите &quot;Удалить&quot; в меню</translation>
</message>
<message>
<location filename="mainscreen_widgets.py" line="382"/>
<location filename="mainscreen_widgets.py" line="378"/>
<source>Use right click on inline image to save it</source>
<translation>Правый клик на инлайн изображении позволит сохранить его</translation>
</message>
@ -828,7 +848,7 @@ Version: </source>
<message>
<location filename="mainscreen_widgets.py" line="370"/>
<source>New in Toxygen v0.2.6:&lt;br&gt;Updater&lt;br&gt;Better contact sorting&lt;br&gt;Plugins improvements</source>
<translation>Новое в Toxygen v0.2.6:&lt;br&gt;Поддержка обновлений&lt;br&gt;Улучшенная сортировка контактов&lt;br&gt;Улучшения в работе плагинов</translation>
<translation type="obsolete">Новое в Toxygen v0.2.6:&lt;br&gt;Поддержка обновлений&lt;br&gt;Улучшенная сортировка контактов&lt;br&gt;Улучшения в работе плагинов</translation>
</message>
</context>
<context>
@ -852,32 +872,32 @@ Version: </source>
<context>
<name>incoming_call</name>
<message>
<location filename="profile.py" line="1214"/>
<location filename="profile.py" line="1229"/>
<source>Incoming video call</source>
<translation>Входящий видеозвонок</translation>
</message>
<message>
<location filename="profile.py" line="1217"/>
<location filename="profile.py" line="1232"/>
<source>Incoming audio call</source>
<translation>Входящий аудиозвонок</translation>
</message>
<message>
<location filename="profile.py" line="1195"/>
<location filename="profile.py" line="1210"/>
<source>Outgoing video call</source>
<translation>Исходящий видеозвонок</translation>
</message>
<message>
<location filename="profile.py" line="1198"/>
<location filename="profile.py" line="1213"/>
<source>Outgoing audio call</source>
<translation>Исходящий аудиозвонок</translation>
</message>
<message>
<location filename="profile.py" line="1248"/>
<location filename="profile.py" line="1263"/>
<source>Call declined</source>
<translation>Звонок отменен</translation>
</message>
<message>
<location filename="profile.py" line="1250"/>
<location filename="profile.py" line="1265"/>
<source>Call finished</source>
<translation>Звонок завершен</translation>
</message>
@ -1038,7 +1058,7 @@ Version: </source>
<translation>Ошибка сохранения профиля! Toxygen имеет разрешение на запись в данную папку?</translation>
</message>
<message>
<location filename="main.py" line="288"/>
<location filename="main.py" line="290"/>
<source>Update for Toxygen was found. Download and install it?</source>
<translation>Обновление для Toxygen было найдено. Загрузить и установить его?</translation>
</message>
@ -1077,7 +1097,7 @@ Version: </source>
<message>
<location filename="passwordscreen.py" line="65"/>
<source>Password:</source>
<translation>Пароль</translation>
<translation>Пароль:</translation>
</message>
<message>
<location filename="passwordscreen.py" line="66"/>
@ -1176,32 +1196,32 @@ Version: </source>
<context>
<name>tray</name>
<message>
<location filename="main.py" line="228"/>
<location filename="main.py" line="229"/>
<source>Open Toxygen</source>
<translation>Открыть Toxygen</translation>
</message>
<message>
<location filename="main.py" line="237"/>
<location filename="main.py" line="238"/>
<source>Exit</source>
<translation>Выход</translation>
</message>
<message>
<location filename="main.py" line="229"/>
<location filename="main.py" line="230"/>
<source>Set status</source>
<translation>Изменить статус</translation>
</message>
<message>
<location filename="main.py" line="230"/>
<location filename="main.py" line="231"/>
<source>Online</source>
<translation>Онлайн</translation>
</message>
<message>
<location filename="main.py" line="231"/>
<location filename="main.py" line="232"/>
<source>Away</source>
<translation>Нет на месте</translation>
</message>
<message>
<location filename="main.py" line="232"/>
<location filename="main.py" line="233"/>
<source>Busy</source>
<translation>Занят</translation>
</message>

View File

@ -1,9 +1,25 @@
# -*- coding: utf-8 -*-
import os
import time
import shutil
import sys
import re
program_version = '0.2.6'
program_version = '0.2.8'
def cached(func):
saved_result = None
def wrapped_func():
nonlocal saved_result
if saved_result is None:
saved_result = func()
return saved_result
return wrapped_func
def log(data):
@ -11,6 +27,7 @@ def log(data):
fl.write(str(data) + '\n')
@cached
def curr_directory():
return os.path.dirname(os.path.realpath(__file__))
@ -37,7 +54,7 @@ def remove(folder):
def convert_time(t):
offset = time.timezone - time.daylight * 3600
offset = time.timezone + time_offset() * 60
sec = int(t) - offset
m, s = divmod(sec, 60)
h, m = divmod(m, 60)
@ -45,6 +62,18 @@ def convert_time(t):
return '%02d:%02d' % (h, m)
@cached
def time_offset():
hours = int(time.strftime('%H'))
minutes = int(time.strftime('%M'))
sec = int(time.time()) - time.timezone
m, s = divmod(sec, 60)
h, m = divmod(m, 60)
d, h = divmod(h, 24)
result = hours * 60 + minutes - h * 60 - m
return result
def append_slash(s):
if len(s) and s[-1] not in ('\\', '/'):
s += '/'
@ -55,6 +84,15 @@ def is_64_bit():
return sys.maxsize > 2 ** 32
def is_re_valid(regex):
try:
re.compile(regex)
except re.error:
return False
else:
return True
class Singleton:
_instance = None

View File

@ -9,7 +9,7 @@ class DataLabel(QtGui.QLabel):
Label with elided text
"""
def setText(self, text):
text = ''.join(c if c <= '\U0010FFFF' else '\u25AF' for c in text)
text = ''.join('\u25AF' if len(bytes(c, 'utf-8')) >= 4 else c for c in text)
metrics = QtGui.QFontMetrics(self.font())
text = metrics.elidedText(text, QtCore.Qt.ElideRight, self.width())
super().setText(text)
@ -137,4 +137,3 @@ class MultilineEdit(CenteredWidget):
def button_click(self):
self.save(self.edit.toPlainText())
self.close()