19 Commits

Author SHA1 Message Date
02507ab8b4 v 0.1.3 2016-05-28 22:43:51 +03:00
c27c35fcc0 docs and translations update 2016-05-28 13:30:26 +03:00
a214ab12c7 merge with plugins 2016-05-28 13:06:13 +03:00
b2fa484bb3 avatars rest fix 2016-05-25 16:05:16 +03:00
be4a592599 history fix 2016-05-24 23:53:40 +03:00
cfdf145160 pyqt4 support 2016-05-24 21:22:21 +03:00
948776c31c tray menu update 2016-05-24 21:08:52 +03:00
b2210c21cf ui update 2016-05-19 00:38:21 +03:00
3a835ceb7e password minimum length 2016-05-16 15:25:51 +03:00
21d5eed719 db and settings encryption 2016-05-15 23:02:05 +03:00
0fa7ca7b5c encrypted .tox files support 2016-05-15 19:54:44 +03:00
010c15a498 password screen and main.py update 2016-05-15 17:39:49 +03:00
284311a91c toxencryptsave finished 2016-05-15 16:45:05 +03:00
a78f18cce5 profile password field in settings 2016-05-15 16:06:41 +03:00
d0b767c779 toxencryptsave bug fixes, tests update 2016-05-15 13:39:03 +03:00
f0875b0415 tox encrypt save 2016-05-15 11:00:45 +03:00
c5ec90a8a4 settings - docs and fixes 2016-05-14 13:35:31 +03:00
1734ea02f9 profile load and saving update 2016-05-14 13:18:17 +03:00
b0e1aeb0f0 toxencryptsave lib loading 2016-05-14 12:29:54 +03:00
33 changed files with 1907 additions and 458 deletions

View File

@ -5,7 +5,7 @@ Toxygen is cross-platform [Tox](https://tox.chat/) client written on Python
[![Open issues](https://img.shields.io/github/issues/xveduk/toxygen.svg?style=flat)](https://github.com/xveduk/toxygen/issues)
[![License](https://img.shields.io/badge/license-GPLv3-blue.svg?style=flat)](https://raw.githubusercontent.com/xveduk/toxygen/master/LICENSE.md)
### [Install](/docs/install.md) - [Contribute](/docs/contributing.md)
### [Install](/docs/install.md) - [Contribute](/docs/contributing.md) - [Plugins](/docs/plugins.md)
### Supported OS:
- Windows
@ -16,8 +16,9 @@ Toxygen is cross-platform [Tox](https://tox.chat/) client written on Python
- [x] File transfers
- [x] Audio
- [x] Chat history
- [x] Name lookups (TOX DNS 4 support)
- [x] Profile import/export
- [x] Screenshots
- [x] Name lookups (toxme.io support)
- [x] Profile import and export
- [x] Inline images
- [x] Message splitting
- [x] Proxy support
@ -30,9 +31,9 @@ Toxygen is cross-platform [Tox](https://tox.chat/) client written on Python
- [x] Typing notifications
- [x] Changing nospam
- [x] File resuming
- [x] Save file encryption
- [x] Plugins support
- [ ] Video
- [ ] Save file encryption
- [ ] Plugins support
- [ ] Group chats
- [ ] Read receipts
- [ ] Faux offline messaging

53
docs/plugin_api.md Normal file
View File

@ -0,0 +1,53 @@
#Plugins API
In Toxygen plugin is single python 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.
Every plugin has it's own full name and unique short name (1-5 symbols). Main app can get it using special methods.
All plugin's data should be stored in following structure:
```
/plugins/
|---plugin_short_name.py
|---/plugin_short_name/
|---settings.json
|---logs.txt
|---other_files
```
Plugin can override following methods:
- get_description - this method should return plugin description.
- get_menu - plugins allowed to add items in friend menu. You can open this menu making right click on friend in friends list. This method should return list of QAction's. Plugin must connect to QAction's triggered() signal.
- get_window - plugins can have GUI, this method should return window instance or None for plugins without GUI.
- start - plugin was started.
- stop - plugin was stopped.
- close - app is closing, stop plugin.
- command - new command to plugin. Command can be entered in message field in format '/plugin <plugin_short_name> <command>'. Command 'help' should show user list of supported commands.
- lossless_packet - callback - incoming lossless packet from friend.
- lossy_packet - callback - incoming lossy packet from friend.
- friend_connected - callback - friend became online.
Other methods:
- send_lossless - this method send custom lossless packet. Plugins MUST send lossless packets using this method.
- send_lossy - this method send custom lossy packet. Plugins MUST send lossy packets using this method.
- load_settings - loads settings stored in default location.
- save_settings - saves settings to default location.
- load_translator - loads translations. Translations must be stored in directory with plugin's data. Files with translations must have the same name as in main app.
About import:
import statement will not work in case you import module that wasn't previously imported by main program and user use precompiled binary. It's recommended to use imp module and dynamic import instead.
About GUI:
It's strictly recommended to support both PySide and PyQt4 in GUI.
Exceptions:
Plugin's methods should not raise exceptions.
#Examples
You can find example of a plugin in [official repo](https://github.com/ingvar1995/toxygen_plugins)

20
docs/plugins.md Normal file
View File

@ -0,0 +1,20 @@
#Plugins
Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python module and directory with plugin's data which provide some additional functionality.
#How to write plugin
Check [Plugin API](/docs/plugin_api.md) for more info
#How to install plugin
Toxygen comes without preinstalled plugins.
1. Put plugin and directory with it's data into /src/plugins/
2. Restart Toxygen
#Plugins list
WARNING: It is unsecure to install plugin not from this list!
[Main repo](https://github.com/ingvar1995/toxygen_plugins)

View File

@ -1,4 +1,7 @@
from PySide import QtCore, QtGui
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
import widgets
import profile
import util

View File

@ -1,11 +1,14 @@
from PySide import QtCore
try:
from PySide import QtCore
except ImportError:
from PyQt4 import QtCore
from notifications import *
from settings import Settings
from profile import Profile
from toxcore_enums_and_consts import *
from toxav_enums import *
from tox import bin_to_string
from ctypes import c_char_p, cast, pointer
from plugin_support import PluginLoader
class InvokeEvent(QtCore.QEvent):
@ -82,6 +85,7 @@ def friend_connection_status(tox, friend_num, new_status, user_data):
sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS'])
elif friend.status is None:
invoke_in_main_thread(profile.send_avatar, friend_num)
PluginLoader.get_instance().friend_online(friend_num)
def friend_name(tox, friend_num, name, size, user_data):
@ -218,13 +222,35 @@ def file_recv_control(tox, friend_number, file_number, file_control, user_data):
elif file_control == TOX_FILE_CONTROL['RESUME']:
Profile.get_instance().resume_transfer(friend_number, file_number, True)
# -----------------------------------------------------------------------------------------------------------------
# Callbacks - custom packets
# -----------------------------------------------------------------------------------------------------------------
def lossless_packet(tox, friend_number, data, length, user_data):
"""
Incoming lossless packet
"""
plugin = PluginLoader.get_instance()
invoke_in_main_thread(plugin.callback_lossless, friend_number, data, length)
def lossy_packet(tox, friend_number, data, length, user_data):
"""
Incoming lossy packet
"""
plugin = PluginLoader.get_instance()
invoke_in_main_thread(plugin.callback_lossy, friend_number, data, length)
# -----------------------------------------------------------------------------------------------------------------
# Callbacks - audio
# -----------------------------------------------------------------------------------------------------------------
def call_state(toxav, friend_number, mask, user_data):
"""New call state"""
"""
New call state
"""
print friend_number, mask
if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']:
invoke_in_main_thread(Profile.get_instance().stop_call, friend_number, True)
@ -233,13 +259,17 @@ def call_state(toxav, friend_number, mask, user_data):
def call(toxav, friend_number, audio, video, user_data):
"""Incoming call from friend"""
"""
Incoming call from friend
"""
print friend_number, audio, video
invoke_in_main_thread(Profile.get_instance().incoming_call, audio, video, friend_number)
def callback_audio(toxav, friend_number, samples, audio_samples_per_channel, audio_channels_count, rate, user_data):
"""New audio chunk"""
"""
New audio chunk
"""
print audio_samples_per_channel, audio_channels_count, rate
Profile.get_instance().call.chunk(
''.join(chr(x) for x in samples[:audio_samples_per_channel * 2 * audio_channels_count]),
@ -279,3 +309,6 @@ def init_callbacks(tox, window, tray):
toxav.callback_call(call, 0)
toxav.callback_audio_receive_frame(callback_audio, 0)
tox.callback_friend_lossless_packet(lossless_packet, 0)
tox.callback_friend_lossy_packet(lossy_packet, 0)

View File

@ -4,7 +4,10 @@ from os import remove, rename
from time import time, sleep
from tox import Tox
import settings
from PySide import QtCore
try:
from PySide import QtCore
except ImportError:
from PyQt4 import QtCore
TOX_FILE_TRANSFER_STATE = {
@ -17,7 +20,10 @@ TOX_FILE_TRANSFER_STATE = {
class StateSignal(QtCore.QObject):
try:
signal = QtCore.Signal(int, float) # state and progress
except:
signal = QtCore.pyqtSignal(int, float) # state and progress - pyqt4
class FileTransfer(QtCore.QObject):
@ -236,13 +242,13 @@ class ReceiveAvatar(ReceiveTransfer):
if size > self.MAX_AVATAR_SIZE:
self.send_control(TOX_FILE_CONTROL['CANCEL'])
remove(path + '.tmp')
elif exists(path):
if not size:
elif not size:
self.send_control(TOX_FILE_CONTROL['CANCEL'])
self._file.close()
if exists(path):
remove(path)
remove(path + '.tmp')
else:
elif exists(path):
hash = self.get_file_id()
with open(path) as fl:
data = fl.read()

View File

@ -2,6 +2,8 @@
from sqlite3 import connect
import settings
from os import chdir
import os.path
from toxencryptsave import LibToxEncryptSave
PAGE_SIZE = 42
@ -17,6 +19,15 @@ class History(object):
def __init__(self, name):
self._name = name
chdir(settings.ProfileHelper.get_path())
path = settings.ProfileHelper.get_path() + self._name + '.hstr'
if os.path.exists(path):
decr = LibToxEncryptSave.get_instance()
with open(path, 'rb') as fin:
data = fin.read()
if decr.is_data_encrypted(data):
data = decr.pass_decrypt(data)
with open(path, 'wb') as fout:
fout.write(data)
db = connect(name + '.hstr')
cursor = db.cursor()
cursor.execute('CREATE TABLE IF NOT EXISTS friends('
@ -24,6 +35,16 @@ class History(object):
')')
db.close()
def save(self):
encr = LibToxEncryptSave.get_instance()
if encr.has_password():
path = settings.ProfileHelper.get_path() + self._name + '.hstr'
with open(path, 'rb') as fin:
data = fin.read()
data = encr.pass_encrypt(data)
with open(path, 'wb') as fout:
fout.write(data)
def export(self, directory):
path = settings.ProfileHelper.get_path() + self._name + '.hstr'
new_path = directory + self._name + '.hstr'
@ -31,7 +52,6 @@ class History(object):
data = fin.read()
with open(new_path, 'wb') as fout:
fout.write(data)
print 'History exported to: {}'.format(new_path)
def add_friend_to_db(self, tox_id):
chdir(settings.ProfileHelper.get_path())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 885 B

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
src/images/search.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -6,7 +6,7 @@ class LibToxCore(object):
def __init__(self):
if system() == 'Linux':
# be sure that libtoxcore and libsodium are installed in your os
# libtoxcore and libsodium must be installed in your os
self._libtoxcore = CDLL('libtoxcore.so')
elif system() == 'Windows':
self._libtoxcore = CDLL('libs/libtox.dll')
@ -21,7 +21,7 @@ class LibToxAV(object):
def __init__(self):
if system() == 'Linux':
# be sure that /usr/lib/libtoxav.so exists
# that /usr/lib/libtoxav.so must exists
self._libtoxav = CDLL('libtoxav.so')
elif system() == 'Windows':
# on Windows av api is in libtox.dll
@ -31,3 +31,19 @@ class LibToxAV(object):
def __getattr__(self, item):
return self._libtoxav.__getattr__(item)
class LibToxEncryptSave(object):
def __init__(self):
if system() == 'Linux':
# /usr/lib/libtoxencryptsave.so must exists
self._lib_tox_encrypt_save = CDLL('libtoxencryptsave.so')
elif system() == 'Windows':
# on Windows profile encryption api is in libtox.dll
self._lib_tox_encrypt_save = CDLL('libs/libtox.dll')
else:
raise OSError('Unknown system.')
def __getattr__(self, item):
return self._lib_tox_encrypt_save.__getattr__(item)

View File

@ -1,5 +1,8 @@
from toxcore_enums_and_consts import *
from PySide import QtGui, QtCore
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
import profile
from file_transfers import TOX_FILE_TRANSFER_STATE
from util import curr_directory, convert_time

View File

@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
from PySide import QtCore, QtGui
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
from widgets import *
@ -22,6 +25,7 @@ class LoginScreen(CenteredWidget):
def __init__(self):
super(LoginScreen, self).__init__()
self.initUI()
self.center()
def initUI(self):
self.resize(400, 200)
@ -91,7 +95,7 @@ class LoginScreen(CenteredWidget):
def update_select(self, data):
list_of_profiles = []
for elem in data:
list_of_profiles.append(self.tr(elem))
list_of_profiles.append(elem)
self.comboBox.addItems(list_of_profiles)
self.create_only = not list_of_profiles

View File

@ -1,7 +1,10 @@
import sys
from loginscreen import LoginScreen
from settings import *
from PySide import QtCore, QtGui
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
from bootstrap import node_generator
from mainscreen import MainWindow
from profile import tox_factory
@ -9,6 +12,10 @@ from callbacks import init_callbacks
from util import curr_directory, get_style
import styles.style
import locale
import toxencryptsave
from passwordscreen import PasswordScreen
import profile
from plugin_support import PluginLoader
class Toxygen(object):
@ -18,21 +25,41 @@ class Toxygen(object):
self.tox = self.ms = self.init = self.mainloop = self.avloop = None
self.path = path
def enter_pass(self, data):
"""
Show password screen
"""
tmp = [data]
p = PasswordScreen(toxencryptsave.LibToxEncryptSave.get_instance(), tmp)
p.show()
self.app.connect(self.app, QtCore.SIGNAL("lastWindowClosed()"), self.app, QtCore.SLOT("quit()"))
self.app.exec_()
if tmp[0] == data:
raise SystemExit()
else:
return tmp[0]
def main(self):
"""
Main function of app. loads login screen if needed and starts main screen
"""
app = QtGui.QApplication(sys.argv)
app.setWindowIcon(QtGui.QIcon(curr_directory() + '/images/icon.png'))
self.app = app
# application color scheme
with open(curr_directory() + '/styles/style.qss') as fl:
dark_style = fl.read()
app.setStyleSheet(dark_style)
encrypt_save = toxencryptsave.LibToxEncryptSave()
if self.path is not None:
path = os.path.dirname(self.path.encode(locale.getpreferredencoding())) + '/'
name = os.path.basename(self.path.encode(locale.getpreferredencoding()))[:-4]
data = ProfileHelper.open_profile(path, name)
data = ProfileHelper(path, name).open_profile()
if encrypt_save.is_data_encrypted(data):
data = self.enter_pass(data)
settings = Settings(name)
self.tox = tox_factory(data, settings)
else:
@ -64,20 +91,24 @@ class Toxygen(object):
self.tox = tox_factory()
self.tox.self_set_name(_login.name if _login.name else 'Toxygen User')
self.tox.self_set_status_message('Toxing on Toxygen')
ProfileHelper.save_profile(self.tox.get_savedata(), name)
ProfileHelper(Settings.get_default_path(), name).save_profile(self.tox.get_savedata())
path = Settings.get_default_path()
settings = Settings(name)
else: # load existing profile
path, name = _login.get_data()
if _login.default:
Settings.set_auto_profile(path, name)
data = ProfileHelper.open_profile(path, name)
data = ProfileHelper(path, name).open_profile()
if encrypt_save.is_data_encrypted(data):
data = self.enter_pass(data)
settings = Settings(name)
self.tox = tox_factory(data, settings)
else:
path, name = auto_profile
path = path.encode(locale.getpreferredencoding())
data = ProfileHelper.open_profile(path, name)
data = ProfileHelper(path, name).open_profile()
if encrypt_save.is_data_encrypted(data):
data = self.enter_pass(data)
settings = Settings(name)
self.tox = tox_factory(data, settings)
@ -105,29 +136,65 @@ class Toxygen(object):
self.tray.setObjectName('tray')
class Menu(QtGui.QMenu):
def newStatus(self, status):
profile.Profile.get_instance().set_status(status)
self.aboutToShow()
self.hide()
def aboutToShow(self):
status = profile.Profile.get_instance().status
act = self.act
if status is None:
self.actions()[1].setVisible(False)
else:
self.actions()[1].setVisible(True)
act.actions()[0].setChecked(False)
act.actions()[1].setChecked(False)
act.actions()[2].setChecked(False)
act.actions()[status].setChecked(True)
def languageChange(self, *args, **kwargs):
self.actions()[0].setText(QtGui.QApplication.translate('tray', 'Open Toxygen', None, QtGui.QApplication.UnicodeUTF8))
self.actions()[1].setText(QtGui.QApplication.translate('tray', 'Exit', None, QtGui.QApplication.UnicodeUTF8))
self.actions()[1].setText(QtGui.QApplication.translate('tray', 'Set status', None, QtGui.QApplication.UnicodeUTF8))
self.actions()[2].setText(QtGui.QApplication.translate('tray', 'Exit', None, QtGui.QApplication.UnicodeUTF8))
self.act.actions()[0].setText(QtGui.QApplication.translate('tray', 'Online', None, QtGui.QApplication.UnicodeUTF8))
self.act.actions()[1].setText(QtGui.QApplication.translate('tray', 'Away', None, QtGui.QApplication.UnicodeUTF8))
self.act.actions()[2].setText(QtGui.QApplication.translate('tray', 'Busy', None, QtGui.QApplication.UnicodeUTF8))
m = Menu()
show = m.addAction(QtGui.QApplication.translate('tray', 'Open Toxygen', None, QtGui.QApplication.UnicodeUTF8))
sub = m.addMenu(QtGui.QApplication.translate('tray', 'Set status', None, QtGui.QApplication.UnicodeUTF8))
onl = sub.addAction(QtGui.QApplication.translate('tray', 'Online', None, QtGui.QApplication.UnicodeUTF8))
away = sub.addAction(QtGui.QApplication.translate('tray', 'Away', None, QtGui.QApplication.UnicodeUTF8))
busy = sub.addAction(QtGui.QApplication.translate('tray', 'Busy', None, QtGui.QApplication.UnicodeUTF8))
onl.setCheckable(True)
away.setCheckable(True)
busy.setCheckable(True)
m.act = sub
exit = m.addAction(QtGui.QApplication.translate('tray', 'Exit', None, QtGui.QApplication.UnicodeUTF8))
def show_window():
if not self.ms.isActiveWindow():
self.ms.setWindowState(self.ms.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
self.ms.activateWindow()
if self.ms.isHidden():
self.ms.show()
m.connect(show, QtCore.SIGNAL("triggered()"), show_window)
m.connect(exit, QtCore.SIGNAL("triggered()"), lambda: app.exit())
m.connect(m, QtCore.SIGNAL("aboutToShow()"), lambda: m.aboutToShow())
sub.connect(onl, QtCore.SIGNAL("triggered()"), lambda: m.newStatus(0))
sub.connect(away, QtCore.SIGNAL("triggered()"), lambda: m.newStatus(1))
sub.connect(busy, QtCore.SIGNAL("triggered()"), lambda: m.newStatus(2))
self.tray.setContextMenu(m)
self.tray.show()
self.ms.show()
QtGui.QApplication.setStyle(get_style(settings['theme'])) # set application style
plugin_helper = PluginLoader(self.tox, settings) # plugin support
plugin_helper.load()
# init thread
self.init = self.InitThread(self.tox, self.ms, self.tray)
self.init.start()
@ -137,16 +204,18 @@ class Toxygen(object):
self.mainloop.start()
self.avloop = self.ToxAVIterateThread(self.tox.AV)
self.avloop.start()
app.connect(app, QtCore.SIGNAL("lastWindowClosed()"), app, QtCore.SLOT("quit()"))
app.exec_()
self.init.stop = True
self.mainloop.stop = True
self.avloop.stop = True
plugin_helper.stop()
self.mainloop.wait()
self.init.wait()
self.avloop.wait()
data = self.tox.get_savedata()
ProfileHelper.save_profile(data)
ProfileHelper.get_instance().save_profile(data)
settings.close()
del self.tox
@ -162,7 +231,7 @@ class Toxygen(object):
self.init.wait()
self.avloop.wait()
data = self.tox.get_savedata()
ProfileHelper.save_profile(data)
ProfileHelper.get_instance().save_profile(data)
del self.tox
# create new tox instance
self.tox = tox_factory(data, Settings.get_instance())
@ -176,6 +245,10 @@ class Toxygen(object):
self.avloop = self.ToxAVIterateThread(self.tox.AV)
self.avloop.start()
plugin_helper = PluginLoader.get_instance()
plugin_helper.set_tox(self.tox)
return self.tox
# -----------------------------------------------------------------------------------------------------------------

View File

@ -3,6 +3,8 @@
from menu import *
from profile import *
from list_items import *
from widgets import QRightClickButton
import plugin_support
class MessageArea(QtGui.QPlainTextEdit):
@ -47,12 +49,16 @@ class MainWindow(QtGui.QMainWindow):
self.menubar.setMinimumSize(self.width(), 25)
self.menubar.setMaximumSize(self.width(), 25)
self.menubar.setBaseSize(self.width(), 25)
self.menuProfile = QtGui.QMenu(self.menubar)
self.menuProfile.setObjectName("menuProfile")
self.menuSettings = QtGui.QMenu(self.menubar)
self.menuSettings.setObjectName("menuSettings")
self.menuPlugins = QtGui.QMenu(self.menubar)
self.menuPlugins.setObjectName("menuPlugins")
self.menuAbout = QtGui.QMenu(self.menubar)
self.menuAbout.setObjectName("menuAbout")
self.actionAdd_friend = QtGui.QAction(MainWindow)
self.actionAdd_friend.setObjectName("actionAdd_friend")
self.actionProfile_settings = QtGui.QAction(MainWindow)
@ -70,6 +76,7 @@ class MainWindow(QtGui.QMainWindow):
self.actionSettings = QtGui.QAction(MainWindow)
self.actionSettings.setObjectName("actionSettings")
self.audioSettings = QtGui.QAction(MainWindow)
self.pluginData = QtGui.QAction(MainWindow)
self.menuProfile.addAction(self.actionAdd_friend)
self.menuProfile.addAction(self.actionSettings)
self.menuSettings.addAction(self.actionPrivacy_settings)
@ -77,9 +84,11 @@ class MainWindow(QtGui.QMainWindow):
self.menuSettings.addAction(self.actionNotifications)
self.menuSettings.addAction(self.actionNetwork)
self.menuSettings.addAction(self.audioSettings)
self.menuPlugins.addAction(self.pluginData)
self.menuAbout.addAction(self.actionAbout_program)
self.menubar.addAction(self.menuProfile.menuAction())
self.menubar.addAction(self.menuSettings.menuAction())
self.menubar.addAction(self.menuPlugins.menuAction())
self.menubar.addAction(self.menuAbout.menuAction())
self.actionAbout_program.triggered.connect(self.about_program)
@ -90,13 +99,15 @@ class MainWindow(QtGui.QMainWindow):
self.actionInterface_settings.triggered.connect(self.interface_settings)
self.actionNotifications.triggered.connect(self.notification_settings)
self.audioSettings.triggered.connect(self.audio_settings)
self.pluginData.triggered.connect(self.plugins_menu)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def languageChange(self, *args, **kwargs):
self.retranslateUi()
def retranslateUi(self):
self.online_contacts.setText(QtGui.QApplication.translate("Form", "Online contacts", None, QtGui.QApplication.UnicodeUTF8))
self.menuPlugins.setTitle(QtGui.QApplication.translate("MainWindow", "Plugins", None, QtGui.QApplication.UnicodeUTF8))
self.pluginData.setText(QtGui.QApplication.translate("MainWindow", "List of plugins", None, QtGui.QApplication.UnicodeUTF8))
self.menuProfile.setTitle(QtGui.QApplication.translate("MainWindow", "Profile", None, QtGui.QApplication.UnicodeUTF8))
self.menuSettings.setTitle(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8))
self.menuAbout.setTitle(QtGui.QApplication.translate("MainWindow", "About", None, QtGui.QApplication.UnicodeUTF8))
@ -109,39 +120,36 @@ class MainWindow(QtGui.QMainWindow):
self.actionAbout_program.setText(QtGui.QApplication.translate("MainWindow", "About program", None, QtGui.QApplication.UnicodeUTF8))
self.actionSettings.setText(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8))
self.audioSettings.setText(QtGui.QApplication.translate("MainWindow", "Audio", None, QtGui.QApplication.UnicodeUTF8))
self.contact_name.setPlaceholderText(QtGui.QApplication.translate("MainWindow", "Find contact", None, QtGui.QApplication.UnicodeUTF8))
self.contact_name.setPlaceholderText(QtGui.QApplication.translate("MainWindow", "Search", None, QtGui.QApplication.UnicodeUTF8))
self.screenshotButton.setToolTip(QtGui.QApplication.translate("MainWindow", "Send screenshot", None, QtGui.QApplication.UnicodeUTF8))
self.fileTransferButton.setToolTip(QtGui.QApplication.translate("MainWindow", "Send file", None, QtGui.QApplication.UnicodeUTF8))
self.sendMessageButton.setToolTip(QtGui.QApplication.translate("MainWindow", "Send message", None, QtGui.QApplication.UnicodeUTF8))
self.callButton.setToolTip(QtGui.QApplication.translate("MainWindow", "Start audio call with friend", None, QtGui.QApplication.UnicodeUTF8))
self.online_contacts.clear()
self.online_contacts.addItem(QtGui.QApplication.translate("MainWindow", "All", None, QtGui.QApplication.UnicodeUTF8))
self.online_contacts.addItem(QtGui.QApplication.translate("MainWindow", "Online", None, QtGui.QApplication.UnicodeUTF8))
self.online_contacts.setCurrentIndex(int(Settings.get_instance()['show_online_friends']))
def setup_right_bottom(self, Form):
Form.setObjectName("right_bottom")
Form.resize(650, 75)
Form.resize(650, 60)
self.messageEdit = MessageArea(Form, self)
self.messageEdit.setGeometry(QtCore.QRect(0, 5, 450, 70))
self.messageEdit.setGeometry(QtCore.QRect(0, 3, 450, 55))
self.messageEdit.setObjectName("messageEdit")
class QRightClickButton(QtGui.QPushButton):
def __init__(self, parent):
super(QRightClickButton, self).__init__(parent)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.RightButton:
self.emit(QtCore.SIGNAL("rightClicked()"))
else:
super(QRightClickButton, self).mousePressEvent(event)
font = QtGui.QFont()
font.setPointSize(10)
self.messageEdit.setFont(font)
self.screenshotButton = QRightClickButton(Form)
self.screenshotButton.setGeometry(QtCore.QRect(455, 5, 55, 70))
self.screenshotButton.setGeometry(QtCore.QRect(455, 3, 55, 55))
self.screenshotButton.setObjectName("screenshotButton")
self.fileTransferButton = QtGui.QPushButton(Form)
self.fileTransferButton.setGeometry(QtCore.QRect(510, 5, 55, 70))
self.fileTransferButton.setGeometry(QtCore.QRect(510, 3, 55, 55))
self.fileTransferButton.setObjectName("fileTransferButton")
self.sendMessageButton = QtGui.QPushButton(Form)
self.sendMessageButton.setGeometry(QtCore.QRect(565, 5, 55, 70))
self.sendMessageButton.setGeometry(QtCore.QRect(565, 3, 60, 55))
self.sendMessageButton.setObjectName("sendMessageButton")
pixmap = QtGui.QPixmap(curr_directory() + '/images/send.png')
@ -151,7 +159,7 @@ class MainWindow(QtGui.QMainWindow):
pixmap = QtGui.QPixmap(curr_directory() + '/images/file.png')
icon = QtGui.QIcon(pixmap)
self.fileTransferButton.setIcon(icon)
self.fileTransferButton.setIconSize(QtCore.QSize(30, 45))
self.fileTransferButton.setIconSize(QtCore.QSize(40, 40))
pixmap = QtGui.QPixmap(curr_directory() + '/images/screenshot.png')
icon = QtGui.QIcon(pixmap)
self.screenshotButton.setIcon(icon)
@ -164,31 +172,36 @@ class MainWindow(QtGui.QMainWindow):
QtCore.QMetaObject.connectSlotsByName(Form)
def setup_left_bottom(self, Form):
Form.setObjectName("left_bottom")
Form.resize(270, 80)
self.online_contacts = QtGui.QCheckBox(Form)
self.online_contacts.setGeometry(QtCore.QRect(0, 0, 250, 20))
self.online_contacts.setObjectName("online_contacts")
self.online_contacts.clicked.connect(self.filtering)
def setup_left_center_menu(self, Form):
Form.resize(270, 25)
self.search_label = QtGui.QLabel(Form)
self.search_label.setGeometry(QtCore.QRect(3, 0, 25, 25))
pixmap = QtGui.QPixmap(QtCore.QSize(25, 25))
pixmap.load(curr_directory() + '/images/search.png')
self.search_label.setScaledContents(False)
self.search_label.setPixmap(pixmap.scaled(25, 25, QtCore.Qt.KeepAspectRatio))
self.contact_name = QtGui.QLineEdit(Form)
self.contact_name.setGeometry(QtCore.QRect(0, 23, 270, 26))
self.contact_name.setGeometry(QtCore.QRect(30, 0, 120, 25))
self.contact_name.setObjectName("contact_name")
self.contact_name.textChanged.connect(self.filtering)
self.online_contacts = QtGui.QComboBox(Form)
self.online_contacts.setGeometry(QtCore.QRect(150, 0, 120, 25))
self.online_contacts.activated[int].connect(lambda x: self.filtering())
QtCore.QMetaObject.connectSlotsByName(Form)
def setup_left_top(self, Form):
Form.setObjectName("left_top")
Form.resize(500, 300)
Form.setCursor(QtCore.Qt.PointingHandCursor)
Form.setMinimumSize(QtCore.QSize(250, 100))
Form.setMaximumSize(QtCore.QSize(250, 100))
Form.setBaseSize(QtCore.QSize(250, 100))
Form.setMinimumSize(QtCore.QSize(250, 80))
Form.setMaximumSize(QtCore.QSize(250, 80))
Form.setBaseSize(QtCore.QSize(250, 80))
self.avatar_label = Form.avatar_label = QtGui.QLabel(Form)
self.avatar_label.setGeometry(QtCore.QRect(10, 20, 64, 64))
self.avatar_label.setGeometry(QtCore.QRect(5, 15, 64, 64))
self.avatar_label.setScaledContents(True)
self.name = Form.name = DataLabel(Form)
Form.name.setGeometry(QtCore.QRect(80, 30, 150, 25))
Form.name.setGeometry(QtCore.QRect(80, 25, 150, 25))
font = QtGui.QFont()
font.setFamily("Times New Roman")
font.setPointSize(14)
@ -196,13 +209,13 @@ class MainWindow(QtGui.QMainWindow):
Form.name.setFont(font)
Form.name.setObjectName("name")
self.status_message = Form.status_message = DataLabel(Form)
Form.status_message.setGeometry(QtCore.QRect(80, 60, 170, 20))
Form.status_message.setGeometry(QtCore.QRect(80, 55, 170, 20))
font.setPointSize(12)
font.setBold(False)
Form.status_message.setFont(font)
Form.status_message.setObjectName("status_message")
self.connection_status = Form.connection_status = StatusCircle(self)
Form.connection_status.setGeometry(QtCore.QRect(230, 34, 64, 64))
Form.connection_status.setGeometry(QtCore.QRect(230, 29, 64, 64))
Form.connection_status.setMinimumSize(QtCore.QSize(32, 32))
Form.connection_status.setMaximumSize(QtCore.QSize(32, 32))
Form.connection_status.setBaseSize(QtCore.QSize(32, 32))
@ -214,12 +227,12 @@ class MainWindow(QtGui.QMainWindow):
def setup_right_top(self, Form):
Form.setObjectName("Form")
Form.resize(650, 300)
Form.resize(650, 80)
self.account_avatar = QtGui.QLabel(Form)
self.account_avatar.setGeometry(QtCore.QRect(10, 20, 64, 64))
self.account_avatar.setGeometry(QtCore.QRect(10, 17, 64, 64))
self.account_avatar.setScaledContents(True)
self.account_name = DataLabel(Form)
self.account_name.setGeometry(QtCore.QRect(100, 30, 400, 25))
self.account_name.setGeometry(QtCore.QRect(100, 25, 400, 25))
self.account_name.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse)
font = QtGui.QFont()
font.setFamily("Times New Roman")
@ -228,7 +241,7 @@ class MainWindow(QtGui.QMainWindow):
self.account_name.setFont(font)
self.account_name.setObjectName("account_name")
self.account_status = DataLabel(Form)
self.account_status.setGeometry(QtCore.QRect(100, 50, 400, 25))
self.account_status.setGeometry(QtCore.QRect(100, 45, 400, 25))
self.account_status.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse)
font.setPointSize(12)
font.setBold(False)
@ -281,37 +294,39 @@ class MainWindow(QtGui.QMainWindow):
main = QtGui.QWidget()
grid = QtGui.QGridLayout()
search = QtGui.QWidget()
self.setup_left_bottom(search)
grid.addWidget(search, 3, 0)
self.setup_left_center_menu(search)
grid.addWidget(search, 1, 0)
name = QtGui.QWidget()
self.setup_left_top(name)
grid.addWidget(name, 0, 0)
messages = QtGui.QWidget()
self.setup_right_center(messages)
grid.addWidget(messages, 1, 1)
grid.addWidget(messages, 1, 1, 2, 1)
info = QtGui.QWidget()
self.setup_right_top(info)
grid.addWidget(info, 0, 1)
message_buttons = QtGui.QWidget()
self.setup_right_bottom(message_buttons)
grid.addWidget(message_buttons, 2, 1, 2, 1)
grid.addWidget(message_buttons, 3, 1)
main_list = QtGui.QWidget()
self.setup_left_center(main_list)
grid.addWidget(main_list, 1, 0, 2, 1)
grid.addWidget(main_list, 2, 0, 2, 1)
grid.setColumnMinimumWidth(1, 500)
grid.setColumnMinimumWidth(0, 270)
grid.setRowMinimumHeight(1, 400)
grid.setRowMinimumHeight(2, 20)
grid.setRowMinimumHeight(3, 50)
grid.setRowMinimumHeight(0, 82)
grid.setRowMinimumHeight(1, 25)
grid.setRowMinimumHeight(2, 410)
grid.setRowMinimumHeight(3, 60)
grid.setColumnStretch(1, 1)
grid.setRowStretch(1, 1)
main.setLayout(grid)
self.setCentralWidget(main)
self.setup_menu(self)
self.messageEdit.setFocus()
self.user_info = name
self.friend_info = info
self.profile = Profile(tox, self)
self.retranslateUi()
self.profile = Profile(tox, self)
def closeEvent(self, *args, **kwargs):
self.profile.save_history()
@ -319,14 +334,16 @@ class MainWindow(QtGui.QMainWindow):
QtGui.QApplication.closeAllWindows()
def resizeEvent(self, *args, **kwargs):
self.messages.setGeometry(0, 0, self.width() - 300, self.height() - 205)
self.friends_list.setGeometry(0, 0, 270, self.height() - 180)
self.callButton.setGeometry(QtCore.QRect(self.width() - 370, 30, 50, 50))
self.typing.setGeometry(QtCore.QRect(self.width() - 420, 40, 50, 30))
self.messageEdit.setGeometry(QtCore.QRect(0, 5, self.width() - 470, 70))
self.screenshotButton.setGeometry(QtCore.QRect(self.width() - 465, 5, 55, 70))
self.fileTransferButton.setGeometry(QtCore.QRect(self.width() - 410, 5, 55, 70))
self.sendMessageButton.setGeometry(QtCore.QRect(self.width() - 355, 5, 55, 70))
self.messages.setGeometry(0, 0, self.width() - 300, self.height() - 172)
self.friends_list.setGeometry(0, 0, 270, self.height() - 142)
self.callButton.setGeometry(QtCore.QRect(self.width() - 370, 20, 50, 50))
self.typing.setGeometry(QtCore.QRect(self.width() - 420, 30, 50, 30))
self.messageEdit.setGeometry(QtCore.QRect(120, 2, self.width() - 490, 55))
self.screenshotButton.setGeometry(QtCore.QRect(0, 2, 55, 55))
self.fileTransferButton.setGeometry(QtCore.QRect(60, 2, 55, 55))
self.sendMessageButton.setGeometry(QtCore.QRect(self.width() - 360, 2, 60, 55))
self.account_name.setGeometry(QtCore.QRect(100, 30, self.width() - 520, 25))
self.account_status.setGeometry(QtCore.QRect(100, 50, self.width() - 520, 25))
self.profile.update()
@ -353,6 +370,10 @@ class MainWindow(QtGui.QMainWindow):
self.n_s = NetworkSettings(self.reset)
self.n_s.show()
def plugins_menu(self):
self.p_s = PluginsSettings()
self.p_s.show()
def add_contact(self):
self.a_c = AddContact()
self.a_c.show()
@ -426,7 +447,7 @@ class MainWindow(QtGui.QMainWindow):
def friend_right_click(self, pos):
item = self.friends_list.itemAt(pos)
num = self.friends_list.indexFromItem(item).row()
friend = Profile.get_instance().get_friend_by_number(num)
friend = Profile.get_instance().get_friend(num)
settings = Settings.get_instance()
allowed = friend.tox_id in settings['auto_accept_from_friends']
auto = QtGui.QApplication.translate("MainWindow", 'Disallow auto accept', None, QtGui.QApplication.UnicodeUTF8) if allowed else QtGui.QApplication.translate("MainWindow", 'Allow auto accept', None, QtGui.QApplication.UnicodeUTF8)
@ -437,6 +458,10 @@ class MainWindow(QtGui.QMainWindow):
copy_key_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Copy public key', None, QtGui.QApplication.UnicodeUTF8))
auto_accept_item = self.listMenu.addAction(auto)
remove_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Remove friend', 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)
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(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num))
@ -486,7 +511,7 @@ class MainWindow(QtGui.QMainWindow):
super(self.__class__, self).mouseReleaseEvent(event)
def filtering(self):
self.profile.filtration(self.online_contacts.isChecked(), self.contact_name.text())
self.profile.filtration(self.online_contacts.currentIndex() == 1, self.contact_name.text())
class ScreenShotWindow(QtGui.QWidget):

View File

@ -1,9 +1,14 @@
from PySide import QtCore, QtGui
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
from settings import *
from profile import Profile
from util import get_style, curr_directory
from widgets import CenteredWidget, DataLabel
import pyaudio
import toxencryptsave
import plugin_support
class AddContact(CenteredWidget):
@ -72,11 +77,12 @@ class ProfileSettings(CenteredWidget):
def __init__(self):
super(ProfileSettings, self).__init__()
self.initUI()
self.center()
def initUI(self):
self.setObjectName("ProfileSettingsForm")
self.setMinimumSize(QtCore.QSize(650, 320))
self.setMaximumSize(QtCore.QSize(650, 320))
self.setMinimumSize(QtCore.QSize(650, 520))
self.setMaximumSize(QtCore.QSize(650, 520))
self.nick = QtGui.QLineEdit(self)
self.nick.setGeometry(QtCore.QRect(30, 60, 350, 27))
self.nick.setObjectName("nick")
@ -126,6 +132,28 @@ class ProfileSettings(CenteredWidget):
self.delete_avatar.setGeometry(QtCore.QRect(400, 120, 200, 50))
self.delete_avatar.clicked.connect(self.reset_avatar)
self.new_avatar.clicked.connect(self.set_avatar)
self.profile_pass = QtGui.QLabel(self)
self.profile_pass.setGeometry(QtCore.QRect(40, 300, 300, 50))
font.setPointSize(18)
self.profile_pass.setFont(font)
self.password = QtGui.QLineEdit(self)
self.password.setGeometry(QtCore.QRect(30, 350, 300, 30))
self.password.setEchoMode(QtGui.QLineEdit.EchoMode.Password)
self.leave_blank = QtGui.QLabel(self)
self.leave_blank.setGeometry(QtCore.QRect(340, 350, 300, 30))
self.confirm_password = QtGui.QLineEdit(self)
self.confirm_password.setGeometry(QtCore.QRect(30, 400, 300, 30))
self.confirm_password.setEchoMode(QtGui.QLineEdit.EchoMode.Password)
self.set_password = QtGui.QPushButton(self)
self.set_password.setGeometry(QtCore.QRect(30, 450, 300, 30))
self.set_password.clicked.connect(self.new_password)
self.not_match = QtGui.QLabel(self)
self.not_match.setGeometry(QtCore.QRect(340, 400, 300, 30))
self.not_match.setVisible(False)
self.not_match.setStyleSheet('QLabel { color: #F70D1A; }')
self.warning = QtGui.QLabel(self)
self.warning.setGeometry(QtCore.QRect(30, 490, 500, 30))
self.warning.setStyleSheet('QLabel { color: #F70D1A; }')
self.retranslateUi()
QtCore.QMetaObject.connectSlotsByName(self)
@ -139,6 +167,29 @@ class ProfileSettings(CenteredWidget):
self.new_avatar.setText(QtGui.QApplication.translate("ProfileSettingsForm", "New avatar", None, QtGui.QApplication.UnicodeUTF8))
self.delete_avatar.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Reset avatar", None, QtGui.QApplication.UnicodeUTF8))
self.new_nospam.setText(QtGui.QApplication.translate("ProfileSettingsForm", "New NoSpam", None, QtGui.QApplication.UnicodeUTF8))
self.profile_pass.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Profile password", None, QtGui.QApplication.UnicodeUTF8))
self.password.setPlaceholderText(QtGui.QApplication.translate("ProfileSettingsForm", "Password (at least 8 symbols)", None, QtGui.QApplication.UnicodeUTF8))
self.confirm_password.setPlaceholderText(QtGui.QApplication.translate("ProfileSettingsForm", "Confirm password", None, QtGui.QApplication.UnicodeUTF8))
self.set_password.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Set password", None, QtGui.QApplication.UnicodeUTF8))
self.not_match.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Passwords do not match", None, QtGui.QApplication.UnicodeUTF8))
self.leave_blank.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Leaving blank will reset current password", None, QtGui.QApplication.UnicodeUTF8))
self.warning.setText(QtGui.QApplication.translate("ProfileSettingsForm", "There is no way to recover lost passwords", None, QtGui.QApplication.UnicodeUTF8))
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.LibToxEncryptSave.get_instance()
e.set_password(self.password.text())
self.close()
else:
self.not_match.setText(
QtGui.QApplication.translate("ProfileSettingsForm", "Password must be at least 8 symbols", None,
QtGui.QApplication.UnicodeUTF8))
self.not_match.setVisible(True)
else:
self.not_match.setText(QtGui.QApplication.translate("ProfileSettingsForm", "Passwords do not match", None,
QtGui.QApplication.UnicodeUTF8))
self.not_match.setVisible(True)
def copy(self):
clipboard = QtGui.QApplication.clipboard()
@ -156,7 +207,8 @@ class ProfileSettings(CenteredWidget):
Profile.get_instance().reset_avatar()
def set_avatar(self):
name = QtGui.QFileDialog.getOpenFileName(self, 'Open file', None, 'Image Files (*.png)')
choose = QtGui.QApplication.translate("ProfileSettingsForm", "Choose avatar", None, QtGui.QApplication.UnicodeUTF8)
name = QtGui.QFileDialog.getOpenFileName(self, choose, None, 'Image Files (*.png)')
if name[0]:
with open(name[0], 'rb') as f:
data = f.read()
@ -165,7 +217,7 @@ class ProfileSettings(CenteredWidget):
def export_profile(self):
directory = QtGui.QFileDialog.getExistingDirectory() + '/'
if directory != '/':
ProfileHelper.export_profile(directory)
ProfileHelper.get_instance().export_profile(directory)
settings = Settings.get_instance()
settings.export(directory)
profile = Profile.get_instance()
@ -183,13 +235,14 @@ class NetworkSettings(CenteredWidget):
super(NetworkSettings, self).__init__()
self.reset = reset
self.initUI()
self.center()
def initUI(self):
self.setObjectName("NetworkSettings")
self.resize(300, 300)
self.setMinimumSize(QtCore.QSize(300, 300))
self.setMaximumSize(QtCore.QSize(300, 300))
self.setBaseSize(QtCore.QSize(300, 300))
self.resize(300, 330)
self.setMinimumSize(QtCore.QSize(300, 330))
self.setMaximumSize(QtCore.QSize(300, 330))
self.setBaseSize(QtCore.QSize(300, 330))
self.ipv = QtGui.QCheckBox(self)
self.ipv.setGeometry(QtCore.QRect(20, 10, 97, 22))
self.ipv.setObjectName("ipv")
@ -221,6 +274,9 @@ class NetworkSettings(CenteredWidget):
self.proxyip.setText(settings['proxy_host'])
self.proxyport.setText(unicode(settings['proxy_port']))
self.http.setChecked(settings['proxy_type'] == 1)
self.warning = QtGui.QLabel(self)
self.warning.setGeometry(QtCore.QRect(5, 270, 290, 60))
self.warning.setStyleSheet('QLabel { color: #F70D1A; }')
self.retranslateUi()
self.proxy.stateChanged.connect(lambda x: self.activate())
self.activate()
@ -235,6 +291,8 @@ class NetworkSettings(CenteredWidget):
self.label_2.setText(QtGui.QApplication.translate("Form", "Port:", None, QtGui.QApplication.UnicodeUTF8))
self.reconnect.setText(QtGui.QApplication.translate("NetworkSettings", "Restart TOX core", None, QtGui.QApplication.UnicodeUTF8))
self.http.setText(QtGui.QApplication.translate("Form", "HTTP", None, QtGui.QApplication.UnicodeUTF8))
self.warning.setText(QtGui.QApplication.translate("Form", "WARNING:\nusing proxy with enabled UDP\ncan produce IP leak",
None, QtGui.QApplication.UnicodeUTF8))
def activate(self):
bl = self.proxy.isChecked()
@ -254,8 +312,8 @@ class NetworkSettings(CenteredWidget):
# recreate tox instance
Profile.get_instance().reset(self.reset)
self.close()
except:
pass
except Exception as ex:
log('Exception in restart: ' + str(ex))
class PrivacySettings(CenteredWidget):
@ -264,6 +322,7 @@ class PrivacySettings(CenteredWidget):
def __init__(self):
super(PrivacySettings, self).__init__()
self.initUI()
self.center()
def initUI(self):
self.setObjectName("privacySettings")
@ -372,6 +431,7 @@ class NotificationsSettings(CenteredWidget):
def __init__(self):
super(NotificationsSettings, self).__init__()
self.initUI()
self.center()
def initUI(self):
self.setObjectName("notificationsForm")
@ -417,6 +477,7 @@ class InterfaceSettings(CenteredWidget):
def __init__(self):
super(InterfaceSettings, self).__init__()
self.initUI()
self.center()
def initUI(self):
self.setObjectName("interfaceForm")
@ -484,6 +545,7 @@ class AudioSettings(CenteredWidget):
super(AudioSettings, self).__init__()
self.initUI()
self.retranslateUi()
self.center()
def initUI(self):
self.setObjectName("audioSettingsForm")
@ -528,3 +590,76 @@ class AudioSettings(CenteredWidget):
settings.audio['input'] = self.in_indexes[self.input.currentIndex()]
settings.audio['output'] = self.out_indexes[self.output.currentIndex()]
settings.save()
class PluginsSettings(CenteredWidget):
def __init__(self):
super(PluginsSettings, self).__init__()
self.initUI()
self.center()
self.retranslateUi()
def initUI(self):
self.resize(400, 210)
self.setMinimumSize(QtCore.QSize(400, 210))
self.setMaximumSize(QtCore.QSize(400, 210))
self.comboBox = QtGui.QComboBox(self)
self.comboBox.setGeometry(QtCore.QRect(30, 10, 340, 30))
self.label = QtGui.QLabel(self)
self.label.setGeometry(QtCore.QRect(30, 40, 340, 90))
self.label.setWordWrap(True)
self.button = QtGui.QPushButton(self)
self.button.setGeometry(QtCore.QRect(30, 130, 340, 30))
self.button.clicked.connect(self.button_click)
self.open = QtGui.QPushButton(self)
self.open.setGeometry(QtCore.QRect(30, 170, 340, 30))
self.open.clicked.connect(self.open_plugin)
self.pl_loader = plugin_support.PluginLoader.get_instance()
self.update_list()
self.comboBox.currentIndexChanged.connect(self.show_data)
self.show_data()
def retranslateUi(self):
self.setWindowTitle(QtGui.QApplication.translate('PluginsForm', "Plugins", None, QtGui.QApplication.UnicodeUTF8))
self.open.setText(QtGui.QApplication.translate('PluginsForm', "Open selected plugin", None, QtGui.QApplication.UnicodeUTF8))
def open_plugin(self):
ind = self.comboBox.currentIndex()
plugin = self.data[ind]
window = self.pl_loader.plugin_window(plugin[-1])
if window is not None:
self.window = window
self.window.show()
else:
msgBox = QtGui.QMessageBox()
text = (QtGui.QApplication.translate("PluginsForm", 'No GUI found for this plugin', None,
QtGui.QApplication.UnicodeUTF8))
msgBox.setText(text)
msgBox.exec_()
def update_list(self):
self.comboBox.clear()
data = self.pl_loader.get_plugins_list()
self.comboBox.addItems(map(lambda x: x[0], data))
self.data = data
def show_data(self):
ind = self.comboBox.currentIndex()
plugin = self.data[ind]
descr = plugin[2] or QtGui.QApplication.translate("PluginsForm", "No description available", None, QtGui.QApplication.UnicodeUTF8)
self.label.setText(descr)
if plugin[1]:
self.button.setText(QtGui.QApplication.translate("PluginsForm", "Disable plugin", None, QtGui.QApplication.UnicodeUTF8))
else:
self.button.setText(QtGui.QApplication.translate("PluginsForm", "Enable plugin", None, QtGui.QApplication.UnicodeUTF8))
def button_click(self):
ind = self.comboBox.currentIndex()
plugin = self.data[ind]
self.pl_loader.toggle_plugin(plugin[-1])
plugin[1] = not plugin[1]
if plugin[1]:
self.button.setText(QtGui.QApplication.translate("PluginsForm", "Disable plugin", None, QtGui.QApplication.UnicodeUTF8))
else:
self.button.setText(QtGui.QApplication.translate("PluginsForm", "Enable plugin", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -1,4 +1,7 @@
from PySide import QtGui, QtCore
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
from util import curr_directory
import wave
import pyaudio

76
src/passwordscreen.py Normal file
View File

@ -0,0 +1,76 @@
from widgets import CenteredWidget
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
class PasswordArea(QtGui.QLineEdit):
def __init__(self, parent):
super(PasswordArea, self).__init__(parent)
self.parent = parent
self.setEchoMode(QtGui.QLineEdit.EchoMode.Password)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Return:
self.parent.button_click()
else:
super(PasswordArea, self).keyPressEvent(event)
class PasswordScreen(CenteredWidget):
def __init__(self, encrypt, data):
super(PasswordScreen, self).__init__()
self._encrypt = encrypt
self._data = data
self.initUI()
def initUI(self):
self.resize(360, 170)
self.setMinimumSize(QtCore.QSize(360, 170))
self.setMaximumSize(QtCore.QSize(360, 170))
self.enter_pass = QtGui.QLabel(self)
self.enter_pass.setGeometry(QtCore.QRect(30, 10, 300, 30))
self.password = PasswordArea(self)
self.password.setGeometry(QtCore.QRect(30, 50, 300, 30))
self.button = QtGui.QPushButton(self)
self.button.setGeometry(QtCore.QRect(30, 90, 300, 30))
self.button.setText('OK')
self.button.clicked.connect(self.button_click)
self.warning = QtGui.QLabel(self)
self.warning.setGeometry(QtCore.QRect(30, 130, 300, 30))
self.warning.setStyleSheet('QLabel { color: #F70D1A; }')
self.warning.setVisible(False)
self.retranslateUi()
self.center()
QtCore.QMetaObject.connectSlotsByName(self)
def button_click(self):
if self.password.text():
try:
self._encrypt.set_password(self.password.text())
new_data = self._encrypt.pass_decrypt(self._data[0])
except Exception:
self.warning.setVisible(True)
else:
self._data[0] = new_data
self.close()
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Enter:
self.button_click()
else:
super(PasswordScreen, self).keyPressEvent(event)
def retranslateUi(self):
self.setWindowTitle(QtGui.QApplication.translate("pass", "Enter password", None, QtGui.QApplication.UnicodeUTF8))
self.enter_pass.setText(QtGui.QApplication.translate("pass", "Password:", None, QtGui.QApplication.UnicodeUTF8))
self.warning.setText(QtGui.QApplication.translate("pass", "Incorrect password", None, QtGui.QApplication.UnicodeUTF8))

146
src/plugin_support.py Normal file
View File

@ -0,0 +1,146 @@
import util
import profile
import os
import imp
import inspect
import plugins.plugin_super_class as pl
import toxencryptsave
class PluginLoader(util.Singleton):
def __init__(self, tox, settings):
self._profile = profile.Profile.get_instance()
self._settings = settings
self._plugins = {} # dict. key - plugin unique short name, value - tuple (plugin instance, is active)
self._tox = tox
self._encr = toxencryptsave.LibToxEncryptSave.get_instance()
def set_tox(self, tox):
"""
New tox instance
"""
self._tox = tox
for value in self._plugins.values():
value[0].set_tox(tox)
def load(self):
"""
Load all plugins in plugins folder
"""
path = util.curr_directory() + '/plugins/'
files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
for fl in files:
if fl in ('plugin_super_class.py', '__init__.py') or not fl.endswith('.py'):
continue
name = fl[:-3] # module name without .py
try:
module = imp.load_source('plugins.' + name, path + fl) # import plugin
except ImportError:
util.log('Import error in module ' + name)
continue
except Exception as ex:
util.log('Exception in module ' + name + ' Exception: ' + str(ex))
continue
for elem in dir(module):
obj = getattr(module, elem)
if inspect.isclass(obj) and issubclass(obj, pl.PluginSuperClass): # looking for plugin class in module
print 'Plugin', elem
try: # create instance of plugin class
inst = obj(self._tox, self._profile, self._settings, self._encr)
autostart = inst.get_short_name() in self._settings['plugins']
if autostart:
inst.start()
except Exception as ex:
util.log('Exception in module ' + name + ' Exception: ' + str(ex))
continue
self._plugins[inst.get_short_name()] = [inst, autostart] # (inst, is active)
break
def callback_lossless(self, friend_number, data, length):
"""
New incoming custom lossless packet (callback)
"""
l = data[0] - pl.LOSSLESS_FIRST_BYTE
name = ''.join(chr(x) for x in data[1:l + 1])
if name in self._plugins and self._plugins[name][1]:
self._plugins[name][0].lossless_packet(''.join(chr(x) for x in data[l + 1:length]), friend_number)
def callback_lossy(self, friend_number, data, length):
"""
New incoming custom lossy packet (callback)
"""
l = data[0] - pl.LOSSY_FIRST_BYTE
name = ''.join(chr(x) for x in data[1:l + 1])
if name in self._plugins and self._plugins[name][1]:
self._plugins[name][0].lossy_packet(''.join(chr(x) for x in data[l + 1:length]), friend_number)
def friend_online(self, friend_number):
for elem in self._plugins.values():
if elem[1]:
elem[0].friend_connected(friend_number)
def get_plugins_list(self):
"""
Returns list of all plugins
"""
result = []
for data in self._plugins.values():
result.append([data[0].get_name(), # plugin full name
data[1], # is enabled
data[0].get_description(), # plugin description
data[0].get_short_name()]) # key - short unique name
return result
def plugin_window(self, key):
"""
Return window or None for specified plugin
"""
return self._plugins[key][0].get_window()
def toggle_plugin(self, key):
"""
Enable/disable plugin
:param key: plugin short name
"""
plugin = self._plugins[key]
if plugin[1]:
plugin[0].stop()
else:
plugin[0].start()
plugin[1] = not plugin[1]
if plugin[1]:
self._settings['plugins'].append(key)
else:
self._settings['plugins'].remove(key)
self._settings.save()
def command(self, text):
"""
New command for plugin
"""
text = text.strip()
name = text.split()[0]
if name in self._plugins and self._plugins[name][1]:
self._plugins[name][0].command(text[len(name) + 1:])
def get_menu(self, menu, num):
"""
Return list of items for menu
"""
result = []
for elem in self._plugins.values():
if elem[1]:
try:
result.extend(elem[0].get_menu(menu, num))
except:
continue
return result
def stop(self):
"""
App is closing, stop all plugins
"""
for key in self._plugins.keys():
self._plugins[key][0].close()
del self._plugins[key]

0
src/plugins/__init__.py Normal file
View File

View File

@ -0,0 +1,233 @@
import os
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
MAX_SHORT_NAME_LENGTH = 5
LOSSY_FIRST_BYTE = 200
LOSSLESS_FIRST_BYTE = 160
def path_to_data(name):
"""
:param name: plugin unique name
:return path do plugin's directory
"""
return os.path.dirname(os.path.realpath(__file__)) + '/' + name + '/'
def log(name, data):
"""
:param name: plugin unique name
:param data: data for saving in log
"""
with open(path_to_data(name) + 'logs.txt', 'a') as fl:
fl.write(str(data) + '\n')
class PluginSuperClass(object):
"""
Superclass for all plugins. Plugin is python module with at least one class derived from PluginSuperClass.
"""
def __init__(self, name, short_name, tox=None, profile=None, settings=None, encrypt_save=None):
"""
:param name: plugin full name
:param short_name: plugin unique short name (length of short name should not exceed MAX_SHORT_NAME_LENGTH)
:param tox: tox instance
:param profile: profile instance
:param settings: profile settings
:param encrypt_save: LibToxEncryptSave instance.
"""
self._settings = settings
self._profile = profile
self._tox = tox
name = name.strip()
short_name = short_name.strip()
if not name or not short_name:
raise NameError('Wrong name')
self._name = name
self._short_name = short_name[:MAX_SHORT_NAME_LENGTH]
self._translator = None # translator for plugin's GUI
self._encrypt_save = encrypt_save
# -----------------------------------------------------------------------------------------------------------------
# Get methods
# -----------------------------------------------------------------------------------------------------------------
def get_name(self):
"""
:return plugin full name
"""
return self._name
def get_short_name(self):
"""
:return plugin unique (short) name
"""
return self._short_name
def get_description(self):
"""
Should return plugin description
"""
return self.__doc__
def get_menu(self, menu, row_number):
"""
This method creates items for menu which called on right click in list of friends
:param menu: menu instance
:param row_number: number of selected row in list of contacts
:return list of QAction's
"""
return []
def get_window(self):
"""
This method should return window for plugins with GUI or None
"""
return None
def set_tox(self, tox):
"""
New tox instance
"""
self._tox = tox
# -----------------------------------------------------------------------------------------------------------------
# Plugin was stopped, started or new command received
# -----------------------------------------------------------------------------------------------------------------
def start(self):
"""
This method called when plugin was started
"""
pass
def stop(self):
"""
This method called when plugin was stopped
"""
pass
def close(self):
"""
App is closing
"""
pass
def command(self, command):
"""
New command. On 'help' this method should provide user list of available commands
:param command: string with command
"""
msgbox = QtGui.QMessageBox()
title = QtGui.QApplication.translate("PluginWindow", "List of commands for plugin {}", None, QtGui.QApplication.UnicodeUTF8)
msgbox.setWindowTitle(title.format(self._name))
msgbox.setText(QtGui.QApplication.translate("PluginWindow", "No commands available", None, QtGui.QApplication.UnicodeUTF8))
msgbox.exec_()
# -----------------------------------------------------------------------------------------------------------------
# Translations support
# -----------------------------------------------------------------------------------------------------------------
def load_translator(self):
"""
This method loads translations for GUI
"""
app = QtGui.QApplication.instance()
langs = self._settings.supported_languages()
curr_lang = self._settings['language']
if curr_lang in map(lambda x: x[0], langs):
if self._translator is not None:
app.removeTranslator(self._translator)
self._translator = QtCore.QTranslator()
lang_path = filter(lambda x: x[0] == curr_lang, langs)[0][1]
self._translator.load(path_to_data(self._short_name) + lang_path)
app.installTranslator(self._translator)
# -----------------------------------------------------------------------------------------------------------------
# Settings loading and saving
# -----------------------------------------------------------------------------------------------------------------
def load_settings(self):
"""
This method loads settings of plugin and returns raw data
"""
with open(path_to_data(self._short_name) + 'settings.json') as fl:
data = fl.read()
return data
def save_settings(self, data):
"""
This method saves plugin's settings to file
:param data: string with data
"""
with open(path_to_data(self._short_name) + 'settings.json', 'wb') as fl:
fl.write(data)
# -----------------------------------------------------------------------------------------------------------------
# Callbacks
# -----------------------------------------------------------------------------------------------------------------
def lossless_packet(self, data, friend_number):
"""
Incoming lossless packet
:param data: string with data
:param friend_number: number of friend who sent packet
"""
pass
def lossy_packet(self, data, friend_number):
"""
Incoming lossy packet
:param data: string with data
:param friend_number: number of friend who sent packet
"""
pass
def friend_connected(self, friend_number):
"""
Friend with specified number is online now
"""
pass
# -----------------------------------------------------------------------------------------------------------------
# Custom packets sending
# -----------------------------------------------------------------------------------------------------------------
def send_lossless(self, data, friend_number):
"""
This method sends lossless packet to friend
Wrapper for self._tox.friend_send_lossless_packet
Use it instead of direct using self._tox.friend_send_lossless_packet
:return True on success
"""
if data is None:
data = ''
try:
return self._tox.friend_send_lossless_packet(friend_number,
chr(len(self._short_name) + LOSSLESS_FIRST_BYTE) +
self._short_name + str(data))
except:
return False
def send_lossy(self, data, friend_number):
"""
This method sends lossy packet to friend
Wrapper for self._tox.friend_send_lossy_packet
Use it instead of direct using self._tox.friend_send_lossy_packet
:return True on success
"""
if data is None:
data = ''
try:
return self._tox.friend_send_lossy_packet(friend_number,
chr(len(self._short_name) + LOSSY_FIRST_BYTE) +
self._short_name + str(data))
except:
return False

View File

@ -1,5 +1,8 @@
from list_items import MessageItem, ContactItem, FileTransferItem, InlineImageItem
from PySide import QtCore, QtGui
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
from tox import Tox
import os
from messages import *
@ -13,6 +16,7 @@ from file_transfers import *
import time
import calls
import avwidgets
import plugin_support
class Contact(object):
@ -298,7 +302,7 @@ class Profile(Contact, Singleton):
self._incoming_calls = set()
settings = Settings.get_instance()
self._show_online = settings['show_online_friends']
screen.online_contacts.setChecked(self._show_online)
screen.online_contacts.setCurrentIndex(int(self._show_online))
aliases = settings['friends_aliases']
data = tox.self_get_friend_list()
self._history = History(tox.self_get_public_key()) # connection to db
@ -329,8 +333,10 @@ class Profile(Contact, Singleton):
Changes status of user (online, away, busy)
"""
if self._status is not None:
status = (self._status + 1) % 3
super(self.__class__, self).set_status(status)
self.set_status((self._status + 1) % 3)
def set_status(self, status):
super(Profile, self).set_status(status)
self._tox.self_set_status(status)
def set_name(self, value):
@ -378,6 +384,9 @@ class Profile(Contact, Singleton):
def get_friend_by_number(self, num):
return filter(lambda x: x.number == num, self._friends)[0]
def get_friend(self, num):
return self._friends[num]
# -----------------------------------------------------------------------------------------------------------------
# Work with active friend
# -----------------------------------------------------------------------------------------------------------------
@ -552,7 +561,10 @@ class Profile(Contact, Singleton):
Send message to active friend
:param text: message text
"""
if self.is_active_online() and text:
if text.startswith('/plugin '):
plugin_support.PluginLoader.get_instance().command(text[8:])
self._screen.messageEdit.clear()
elif self.is_active_online() and text:
if text.startswith('/me '):
message_type = TOX_MESSAGE_TYPE['ACTION']
text = text[4:]
@ -580,6 +592,7 @@ class Profile(Contact, Singleton):
if not self._history.friend_exists_in_db(friend.tox_id):
self._history.add_friend_to_db(friend.tox_id)
self._history.save_messages_to_db(friend.tox_id, messages)
self._history.save()
del self._history
def clear_history(self, num=None):
@ -742,7 +755,7 @@ class Profile(Contact, Singleton):
else:
self.set_active(0)
data = self._tox.get_savedata()
ProfileHelper.save_profile(data)
ProfileHelper.get_instance().save_profile(data)
def add_friend(self, tox_id):
num = self._tox.friend_add_norequest(tox_id) # num - friend number
@ -772,7 +785,7 @@ class Profile(Contact, Singleton):
num = self._tox.friend_by_public_key(tox_id)
self.delete_friend(num)
data = self._tox.get_savedata()
ProfileHelper.save_profile(data)
ProfileHelper.get_instance().save_profile(data)
except: # not in friend list
pass
@ -788,7 +801,7 @@ class Profile(Contact, Singleton):
if add_to_friend_list:
self.add_friend(tox_id)
data = self._tox.get_savedata()
ProfileHelper.save_profile(data)
ProfileHelper.get_instance().save_profile(data)
# -----------------------------------------------------------------------------------------------------------------
# Friend requests
@ -824,7 +837,7 @@ class Profile(Contact, Singleton):
friend = Friend(message_getter, result, tox_id, '', item, tox_id)
self._friends.append(friend)
data = self._tox.get_savedata()
ProfileHelper.save_profile(data)
ProfileHelper.get_instance().save_profile(data)
return True
except Exception as ex: # wrong data
log('Friend request failed with ' + str(ex))
@ -844,7 +857,7 @@ class Profile(Contact, Singleton):
if reply == QtGui.QMessageBox.Yes: # accepted
self.add_friend(tox_id)
data = self._tox.get_savedata()
ProfileHelper.save_profile(data)
ProfileHelper.get_instance().save_profile(data)
except Exception as ex: # something is wrong
log('Accept friend request failed! ' + str(ex))
@ -868,6 +881,7 @@ class Profile(Contact, Singleton):
friend.status = None
def close(self):
if hasattr(self, '_stop'):
self._call.stop()
del self._call
@ -1018,12 +1032,15 @@ class Profile(Contact, Singleton):
st.set_state_changed_handler(item.update)
self._messages.scrollToBottom()
def send_file(self, path):
def send_file(self, path, number=None):
"""
Send file to current active friend
:param path: file path
:param number: friend_number
"""
friend_number = self.get_active_number()
friend_number = number or self.get_active_number()
if self.get_friend_by_number(friend_number).status is None:
return
st = SendTransfer(path, self._tox, friend_number)
self._file_transfers[(friend_number, st.get_file_number())] = st
tm = TransferMessage(MESSAGE_OWNER['ME'],

View File

@ -4,9 +4,13 @@ import os
import locale
from util import Singleton, curr_directory, log
import pyaudio
from toxencryptsave import LibToxEncryptSave
class Settings(Singleton, dict):
"""
Settings of current profile + global app settings
"""
def __init__(self, name):
self.path = ProfileHelper.get_path() + str(name) + '.json'
@ -14,6 +18,9 @@ class Settings(Singleton, dict):
if os.path.isfile(self.path):
with open(self.path) as fl:
data = fl.read()
inst = LibToxEncryptSave.get_instance()
if inst.has_password():
data = inst.pass_decrypt(data)
try:
info = json.loads(data)
except Exception as ex:
@ -47,6 +54,9 @@ class Settings(Singleton, dict):
@staticmethod
def get_default_settings():
"""
Default profile settings
"""
return {
'theme': 'default',
'ipv6_enabled': True,
@ -69,7 +79,8 @@ class Settings(Singleton, dict):
'friends_aliases': [],
'typing_notifications': False,
'calls_sound': True,
'blocked': []
'blocked': [],
'plugins': []
}
@staticmethod
@ -90,6 +101,9 @@ class Settings(Singleton, dict):
def save(self):
text = json.dumps(self)
inst = LibToxEncryptSave.get_instance()
if inst.has_password():
text = inst.pass_encrypt(text)
with open(self.path, 'w') as fl:
fl.write(text)
@ -108,6 +122,9 @@ class Settings(Singleton, dict):
fl.write(data)
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:
@ -136,12 +153,51 @@ class Settings(Singleton, dict):
return os.getenv('APPDATA') + '/Tox/'
class ProfileHelper(object):
class ProfileHelper(Singleton):
"""
Class with static methods for search, load and save profiles
Class with methods for search, load and save profiles
"""
def __init__(self, path, name):
path = path.decode(locale.getpreferredencoding())
self._path = path + name + '.tox'
self._directory = path
# create /avatars if not exists:
directory = path + 'avatars'
if not os.path.exists(directory):
os.makedirs(directory)
def open_profile(self):
with open(self._path, 'rb') as fl:
data = fl.read()
if data:
return data
else:
raise IOError('Save file has zero size!')
def get_dir(self):
return self._directory
def save_profile(self, data):
inst = LibToxEncryptSave.get_instance()
if inst.has_password():
data = inst.pass_encrypt(data)
with open(self._path, 'wb') as fl:
fl.write(data)
print 'Profile saved successfully'
def export_profile(self, new_path):
new_path += os.path.basename(self._path)
with open(self._path, 'rb') as fin:
data = fin.read()
with open(new_path, 'wb') as fout:
fout.write(data)
print 'Profile exported successfully'
@staticmethod
def find_profiles():
"""
Find available tox profiles
"""
path = Settings.get_default_path()
result = []
# check default path
@ -169,42 +225,8 @@ class ProfileHelper(object):
data = json.loads(data)
if 'active_profile' in data:
return path in data['active_profile']
else:
return False
@staticmethod
def open_profile(path, name):
path = path.decode(locale.getpreferredencoding())
ProfileHelper._path = path + name + '.tox'
ProfileHelper._directory = path
# create /avatars if not exists:
directory = path + 'avatars'
if not os.path.exists(directory):
os.makedirs(directory)
with open(ProfileHelper._path, 'rb') as fl:
data = fl.read()
if data:
return data
else:
raise IOError('Save file has zero size!')
@staticmethod
def save_profile(data, name=None):
if name is not None:
ProfileHelper._path = Settings.get_default_path() + name + '.tox'
ProfileHelper._directory = Settings.get_default_path()
with open(ProfileHelper._path, 'wb') as fl:
fl.write(data)
@staticmethod
def export_profile(new_path):
new_path += os.path.basename(ProfileHelper._path)
with open(ProfileHelper._path, 'rb') as fin:
data = fin.read()
with open(new_path, 'wb') as fout:
fout.write(data)
print 'Data exported to: {}'.format(new_path)
@staticmethod
def get_path():
return ProfileHelper._directory
return ProfileHelper.get_instance().get_dir()

File diff suppressed because one or more lines are too long

View File

@ -1251,9 +1251,7 @@ QPushButton:hover
#friends_list:item:selected
{
border: 2px solid;
background-color: transparent;
border-color: green;
background-color: #333333;
}
#toxygen

110
src/toxencryptsave.py Normal file
View File

@ -0,0 +1,110 @@
import libtox
import util
from ctypes import c_size_t, create_string_buffer, byref, c_int, ArgumentError, c_char_p, c_bool
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 LibToxEncryptSave(util.Singleton):
libtoxencryptsave = libtox.LibToxEncryptSave()
def __init__(self):
self._passphrase = None
def set_password(self, passphrase):
self._passphrase = passphrase
def has_password(self):
return bool(self._passphrase)
def is_data_encrypted(self, data):
func = self.libtoxencryptsave.tox_is_data_encrypted
func.restype = c_bool
result = func(c_char_p(data))
return result
def pass_encrypt(self, data):
"""
Encrypts the given data with the given passphrase.
:return: output array
"""
out = create_string_buffer(len(data) + TOX_PASS_ENCRYPTION_EXTRA_LENGTH)
tox_err_encryption = c_int()
self.libtoxencryptsave.tox_pass_encrypt(c_char_p(data),
c_size_t(len(data)),
c_char_p(self._passphrase),
c_size_t(len(self._passphrase)),
out,
byref(tox_err_encryption))
tox_err_encryption = tox_err_encryption.value
if tox_err_encryption == TOX_ERR_ENCRYPTION['OK']:
return out[:]
elif tox_err_encryption == TOX_ERR_ENCRYPTION['NULL']:
raise ArgumentError('Some input data, or maybe the output pointer, was null.')
elif tox_err_encryption == TOX_ERR_ENCRYPTION['KEY_DERIVATION_FAILED']:
raise RuntimeError('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.')
elif tox_err_encryption == TOX_ERR_ENCRYPTION['FAILED']:
raise RuntimeError('The encryption itself failed.')
def pass_decrypt(self, data):
"""
Decrypts the given data with the given passphrase.
:return: output array
"""
out = create_string_buffer(len(data) - TOX_PASS_ENCRYPTION_EXTRA_LENGTH)
tox_err_decryption = c_int()
self.libtoxencryptsave.tox_pass_decrypt(c_char_p(data),
c_size_t(len(data)),
c_char_p(self._passphrase),
c_size_t(len(self._passphrase)),
out,
byref(tox_err_decryption))
tox_err_decryption = tox_err_decryption.value
if tox_err_decryption == TOX_ERR_DECRYPTION['OK']:
return out[:]
elif tox_err_decryption == TOX_ERR_DECRYPTION['NULL']:
raise ArgumentError('Some input data, or maybe the output pointer, was null.')
elif tox_err_decryption == TOX_ERR_DECRYPTION['INVALID_LENGTH']:
raise ArgumentError('The input data was shorter than TOX_PASS_ENCRYPTION_EXTRA_LENGTH bytes')
elif tox_err_decryption == TOX_ERR_DECRYPTION['BAD_FORMAT']:
raise ArgumentError('The input data is missing the magic number (i.e. wasn\'t created by this module, or is'
' corrupted)')
elif tox_err_decryption == TOX_ERR_DECRYPTION['KEY_DERIVATION_FAILED']:
raise RuntimeError('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.')
elif tox_err_decryption == TOX_ERR_DECRYPTION['FAILED']:
raise RuntimeError('The encrypted byte array could not be decrypted. Either the data was corrupt or the '
'password/key was incorrect.')

View File

@ -1,2 +1,2 @@
SOURCES = main.py profile.py menu.py list_items.py loginscreen.py mainscreen.py
SOURCES = main.py profile.py menu.py list_items.py loginscreen.py mainscreen.py plugins/plugin_super_class.py
TRANSLATIONS = translations/en_GB.ts translations/ru_RU.ts translations/fr_FR.ts

View File

@ -1,20 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.0" language="en">
<!DOCTYPE TS><TS version="1.1" language="en">
<context>
<name>AddContact</name>
<message>
<location filename="menu.py" line="64"/>
<location filename="menu.py" line="69"/>
<source>Add contact</source>
<translation>Add contact</translation>
</message>
<message>
<location filename="menu.py" line="66"/>
<location filename="menu.py" line="71"/>
<source>TOX ID:</source>
<translation>TOX ID:</translation>
</message>
<message>
<location filename="menu.py" line="67"/>
<location filename="menu.py" line="72"/>
<source>Message:</source>
<translation>Message:</translation>
</message>
@ -22,272 +21,395 @@
<context>
<name>Form</name>
<message>
<location filename="menu.py" line="65"/>
<location filename="menu.py" line="70"/>
<source>Send request</source>
<translation>Send request</translation>
</message>
<message>
<location filename="menu.py" line="231"/>
<location filename="menu.py" line="287"/>
<source>IPv6</source>
<translation>IPv6</translation>
</message>
<message>
<location filename="menu.py" line="232"/>
<location filename="menu.py" line="288"/>
<source>UDP</source>
<translation>UDP</translation>
</message>
<message>
<location filename="menu.py" line="233"/>
<location filename="menu.py" line="289"/>
<source>Proxy</source>
<translation>Proxy</translation>
</message>
<message>
<location filename="menu.py" line="234"/>
<location filename="menu.py" line="290"/>
<source>IP:</source>
<translation>IP:</translation>
</message>
<message>
<location filename="menu.py" line="235"/>
<location filename="menu.py" line="291"/>
<source>Port:</source>
<translation>Port:</translation>
</message>
<message>
<location filename="mainscreen.py" line="99"/>
<source>Online contacts</source>
<translation>Online contacts</translation>
<translation type="obsolete">Online contacts</translation>
</message>
<message>
<location filename="menu.py" line="237"/>
<location filename="menu.py" line="293"/>
<source>HTTP</source>
<translation>HTTP</translation>
</message>
<message>
<location filename="menu.py" line="295"/>
<source>WARNING:
using proxy with enabled UDP
can produce IP leak</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="mainscreen.py" line="104"/>
<location filename="mainscreen.py" line="115"/>
<source>Profile</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="110"/>
<location filename="mainscreen.py" line="121"/>
<source>Settings</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="346"/>
<location filename="mainscreen.py" line="363"/>
<source>About</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="103"/>
<location filename="mainscreen.py" line="114"/>
<source>Add contact</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="105"/>
<location filename="mainscreen.py" line="116"/>
<source>Privacy</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="106"/>
<location filename="mainscreen.py" line="117"/>
<source>Interface</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="107"/>
<location filename="mainscreen.py" line="118"/>
<source>Notifications</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="108"/>
<location filename="mainscreen.py" line="119"/>
<source>Network</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="109"/>
<location filename="mainscreen.py" line="120"/>
<source>About program</source>
<translation></translation>
</message>
<message>
<location filename="profile.py" line="840"/>
<location filename="profile.py" line="854"/>
<source>User {} wants to add you to contact list. Message:
{}</source>
<translation></translation>
</message>
<message>
<location filename="profile.py" line="842"/>
<location filename="profile.py" line="856"/>
<source>Friend request</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="390"/>
<location filename="mainscreen.py" line="411"/>
<source>Choose file</source>
<translation>Choose file</translation>
</message>
<message>
<location filename="mainscreen.py" line="431"/>
<location filename="mainscreen.py" line="452"/>
<source>Disallow auto accept</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="432"/>
<location filename="mainscreen.py" line="453"/>
<source>Allow auto accept</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="434"/>
<location filename="mainscreen.py" line="455"/>
<source>Set alias</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="435"/>
<location filename="mainscreen.py" line="456"/>
<source>Clear history</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="436"/>
<location filename="mainscreen.py" line="457"/>
<source>Copy public key</source>
<translation></translation>
</message>
<message>
<location filename="mainscreen.py" line="438"/>
<location filename="mainscreen.py" line="459"/>
<source>Remove friend</source>
<translation></translation>
</message>
<message>
<location filename="profile.py" line="686"/>
<location filename="profile.py" line="700"/>
<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="111"/>
<location filename="mainscreen.py" line="122"/>
<source>Audio</source>
<translation>Audio</translation>
</message>
<message>
<location filename="mainscreen.py" line="112"/>
<source>Find contact</source>
<translation>Find contact</translation>
<translation type="obsolete">Find contact</translation>
</message>
<message>
<location filename="profile.py" line="812"/>
<location filename="profile.py" line="826"/>
<source>Friend added</source>
<translation>Friend added</translation>
</message>
<message>
<location filename="mainscreen.py" line="347"/>
<location filename="mainscreen.py" line="364"/>
<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="813"/>
<location filename="profile.py" line="827"/>
<source>Friend added without sending friend request</source>
<translation>Friend added without sending friend request</translation>
</message>
<message>
<location filename="list_items.py" line="231"/>
<location filename="list_items.py" line="234"/>
<source>Choose folder</source>
<translation>Choose folder</translation>
</message>
<message>
<location filename="mainscreen.py" line="113"/>
<location filename="mainscreen.py" line="124"/>
<source>Send screenshot</source>
<translation>Send screenshot</translation>
</message>
<message>
<location filename="mainscreen.py" line="114"/>
<location filename="mainscreen.py" line="125"/>
<source>Send file</source>
<translation>Send file</translation>
</message>
<message>
<location filename="mainscreen.py" line="115"/>
<location filename="mainscreen.py" line="126"/>
<source>Send message</source>
<translation>Send message</translation>
</message>
<message>
<location filename="mainscreen.py" line="116"/>
<location filename="mainscreen.py" line="127"/>
<source>Start audio call with friend</source>
<translation>Start audio call with friend</translation>
</message>
<message>
<location filename="mainscreen.py" line="462"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="110"/>
<source>List of plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="123"/>
<source>Search</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="129"/>
<source>All</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="130"/>
<source>Online</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NetworkSettings</name>
<message>
<location filename="menu.py" line="230"/>
<location filename="menu.py" line="286"/>
<source>Network settings</source>
<translation>Network settings</translation>
</message>
<message>
<location filename="menu.py" line="236"/>
<location filename="menu.py" line="292"/>
<source>Restart TOX core</source>
<translation>Restart Tox core</translation>
</message>
</context>
<context>
<name>PluginWindow</name>
<message>
<location filename="plugins/plugin_super_class.py" line="121"/>
<source>List of commands for plugin {}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="plugins/plugin_super_class.py" line="122"/>
<source>No commands available</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PluginsForm</name>
<message>
<location filename="menu.py" line="624"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="625"/>
<source>Open selected plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="637"/>
<source>No GUI found for this plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="651"/>
<source>No description available</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="663"/>
<source>Disable plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="665"/>
<source>Enable plugin</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ProfileSettingsForm</name>
<message>
<location filename="menu.py" line="133"/>
<location filename="menu.py" line="161"/>
<source>Export profile</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="134"/>
<location filename="menu.py" line="162"/>
<source>Profile settings</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="135"/>
<location filename="menu.py" line="163"/>
<source>Name:</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="136"/>
<location filename="menu.py" line="164"/>
<source>Status:</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="137"/>
<location filename="menu.py" line="165"/>
<source>TOX ID:</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="138"/>
<location filename="menu.py" line="166"/>
<source>Copy TOX ID</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="139"/>
<location filename="menu.py" line="167"/>
<source>New avatar</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="140"/>
<location filename="menu.py" line="168"/>
<source>Reset avatar</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="141"/>
<location filename="menu.py" line="169"/>
<source>New NoSpam</source>
<translation>New NoSpam</translation>
</message>
<message>
<location filename="menu.py" line="170"/>
<source>Profile password</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="171"/>
<source>Password (at least 8 symbols)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="172"/>
<source>Confirm password</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="173"/>
<source>Set password</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="191"/>
<source>Passwords do not match</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="175"/>
<source>Leaving blank will reset current password</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="176"/>
<source>There is no way to recover lost passwords</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="187"/>
<source>Password must be at least 8 symbols</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="211"/>
<source>Choose avatar</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>audioSettingsForm</name>
<message>
<location filename="menu.py" line="522"/>
<location filename="menu.py" line="584"/>
<source>Audio settings</source>
<translation>Audio settings</translation>
</message>
<message>
<location filename="menu.py" line="523"/>
<location filename="menu.py" line="585"/>
<source>Input device:</source>
<translation>Input device:</translation>
</message>
<message>
<location filename="menu.py" line="524"/>
<location filename="menu.py" line="586"/>
<source>Output device:</source>
<translation>Output device:</translation>
</message>
@ -295,12 +417,12 @@ Version:</translation>
<context>
<name>incoming_call</name>
<message>
<location filename="profile.py" line="1151"/>
<location filename="profile.py" line="1168"/>
<source>Incoming video call</source>
<translation>Incoming video call</translation>
</message>
<message>
<location filename="profile.py" line="1153"/>
<location filename="profile.py" line="1170"/>
<source>Incoming audio call</source>
<translation>Incoming audio call</translation>
</message>
@ -308,17 +430,17 @@ Version:</translation>
<context>
<name>interfaceForm</name>
<message>
<location filename="menu.py" line="460"/>
<location filename="menu.py" line="521"/>
<source>Interface settings</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="461"/>
<location filename="menu.py" line="522"/>
<source>Theme:</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="462"/>
<location filename="menu.py" line="523"/>
<source>Language:</source>
<translation></translation>
</message>
@ -326,47 +448,47 @@ Version:</translation>
<context>
<name>login</name>
<message>
<location filename="loginscreen.py" line="70"/>
<location filename="loginscreen.py" line="74"/>
<source>Log in</source>
<translation></translation>
</message>
<message>
<location filename="loginscreen.py" line="71"/>
<location filename="loginscreen.py" line="75"/>
<source>Create</source>
<translation></translation>
</message>
<message>
<location filename="loginscreen.py" line="72"/>
<location filename="loginscreen.py" line="76"/>
<source>Profile name:</source>
<translation></translation>
</message>
<message>
<location filename="loginscreen.py" line="73"/>
<location filename="loginscreen.py" line="77"/>
<source>Load profile</source>
<translation></translation>
</message>
<message>
<location filename="loginscreen.py" line="74"/>
<location filename="loginscreen.py" line="78"/>
<source>Use as default</source>
<translation></translation>
</message>
<message>
<location filename="loginscreen.py" line="75"/>
<location filename="loginscreen.py" line="79"/>
<source>Load existing profile</source>
<translation></translation>
</message>
<message>
<location filename="loginscreen.py" line="76"/>
<location filename="loginscreen.py" line="80"/>
<source>Create new profile</source>
<translation></translation>
</message>
<message>
<location filename="loginscreen.py" line="77"/>
<location filename="loginscreen.py" line="81"/>
<source>toxygen</source>
<translation></translation>
</message>
<message>
<location filename="main.py" line="87"/>
<location filename="main.py" line="118"/>
<source>Looks like other instance of Toxygen uses this profile! Continue?</source>
<translation></translation>
</message>
@ -374,22 +496,22 @@ Version:</translation>
<context>
<name>notificationsForm</name>
<message>
<location filename="menu.py" line="401"/>
<location filename="menu.py" line="461"/>
<source>Notification settings</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="402"/>
<location filename="menu.py" line="462"/>
<source>Enable notifications</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="403"/>
<location filename="menu.py" line="463"/>
<source>Enable call&apos;s sound</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="404"/>
<location filename="menu.py" line="464"/>
<source>Enable sound notifications</source>
<translation></translation>
</message>
@ -397,77 +519,77 @@ Version:</translation>
<context>
<name>privacySettings</name>
<message>
<location filename="menu.py" line="319"/>
<location filename="menu.py" line="378"/>
<source>Privacy settings</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="320"/>
<location filename="menu.py" line="379"/>
<source>Save chat history</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="321"/>
<location filename="menu.py" line="380"/>
<source>Allow file auto accept</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="322"/>
<location filename="menu.py" line="381"/>
<source>Send typing notifications</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="323"/>
<location filename="menu.py" line="382"/>
<source>Auto accept default path:</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="324"/>
<location filename="menu.py" line="383"/>
<source>Change</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="325"/>
<location filename="menu.py" line="384"/>
<source>Allow inlines</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="348"/>
<location filename="menu.py" line="407"/>
<source>Chat history</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="351"/>
<location filename="menu.py" line="410"/>
<source>History will be cleaned! Continue?</source>
<translation></translation>
</message>
<message>
<location filename="menu.py" line="327"/>
<location filename="menu.py" line="386"/>
<source>Blocked users:</source>
<translation>Blocked users:</translation>
</message>
<message>
<location filename="menu.py" line="328"/>
<location filename="menu.py" line="387"/>
<source>Unblock</source>
<translation>Unblock</translation>
</message>
<message>
<location filename="menu.py" line="329"/>
<location filename="menu.py" line="388"/>
<source>Block user</source>
<translation>Block user</translation>
</message>
<message>
<location filename="menu.py" line="335"/>
<location filename="menu.py" line="394"/>
<source>Add to friend list</source>
<translation>Add to friend list</translation>
</message>
<message>
<location filename="menu.py" line="336"/>
<location filename="menu.py" line="395"/>
<source>Do you want to add this user to friend list?</source>
<translation>Do you want to add this user to friend list?</translation>
</message>
<message>
<location filename="menu.py" line="326"/>
<location filename="menu.py" line="385"/>
<source>Block by TOX ID:</source>
<translation>Block by TOX ID:</translation>
</message>
@ -475,14 +597,34 @@ Version:</translation>
<context>
<name>tray</name>
<message>
<location filename="main.py" line="113"/>
<location filename="main.py" line="166"/>
<source>Open Toxygen</source>
<translation></translation>
</message>
<message>
<location filename="main.py" line="114"/>
<location filename="main.py" line="175"/>
<source>Exit</source>
<translation></translation>
</message>
<message>
<location filename="main.py" line="167"/>
<source>Set status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="168"/>
<source>Online</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="169"/>
<source>Away</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="170"/>
<source>Busy</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -1,20 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="fr">
<!DOCTYPE TS><TS version="1.1" language="fr">
<context>
<name>AddContact</name>
<message>
<location filename="menu.py" line="64"/>
<location filename="menu.py" line="69"/>
<source>Add contact</source>
<translation>Rajouter un contact</translation>
</message>
<message>
<location filename="menu.py" line="66"/>
<location filename="menu.py" line="71"/>
<source>TOX ID:</source>
<translation>ID TOX :</translation>
</message>
<message>
<location filename="menu.py" line="67"/>
<location filename="menu.py" line="72"/>
<source>Message:</source>
<translation>Message :</translation>
</message>
@ -22,101 +21,108 @@
<context>
<name>Form</name>
<message>
<location filename="menu.py" line="65"/>
<location filename="menu.py" line="70"/>
<source>Send request</source>
<translation>Envoyer une demande</translation>
</message>
<message>
<location filename="menu.py" line="231"/>
<location filename="menu.py" line="287"/>
<source>IPv6</source>
<translation>IPv6</translation>
</message>
<message>
<location filename="menu.py" line="232"/>
<location filename="menu.py" line="288"/>
<source>UDP</source>
<translation>UDP</translation>
</message>
<message>
<location filename="menu.py" line="233"/>
<location filename="menu.py" line="289"/>
<source>Proxy</source>
<translation>Proxy</translation>
</message>
<message>
<location filename="menu.py" line="234"/>
<location filename="menu.py" line="290"/>
<source>IP:</source>
<translation>IP :</translation>
</message>
<message>
<location filename="menu.py" line="235"/>
<location filename="menu.py" line="291"/>
<source>Port:</source>
<translation>Port :</translation>
</message>
<message>
<location filename="mainscreen.py" line="99"/>
<source>Online contacts</source>
<translation>Contacts connectés</translation>
<translation type="obsolete">Contacts connectés</translation>
</message>
<message>
<location filename="menu.py" line="237"/>
<location filename="menu.py" line="293"/>
<source>HTTP</source>
<translation>HTTP</translation>
</message>
<message>
<location filename="menu.py" line="295"/>
<source>WARNING:
using proxy with enabled UDP
can produce IP leak</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="mainscreen.py" line="104"/>
<location filename="mainscreen.py" line="115"/>
<source>Profile</source>
<translation>Profile</translation>
</message>
<message>
<location filename="mainscreen.py" line="110"/>
<location filename="mainscreen.py" line="121"/>
<source>Settings</source>
<translation>Paramêtres</translation>
</message>
<message>
<location filename="mainscreen.py" line="346"/>
<location filename="mainscreen.py" line="363"/>
<source>About</source>
<translation>À Propos</translation>
</message>
<message>
<location filename="mainscreen.py" line="103"/>
<location filename="mainscreen.py" line="114"/>
<source>Add contact</source>
<translation>Rajouter un contact</translation>
</message>
<message>
<location filename="mainscreen.py" line="105"/>
<location filename="mainscreen.py" line="116"/>
<source>Privacy</source>
<translation>Confidentialité</translation>
</message>
<message>
<location filename="mainscreen.py" line="106"/>
<location filename="mainscreen.py" line="117"/>
<source>Interface</source>
<translation>Interface</translation>
</message>
<message>
<location filename="mainscreen.py" line="107"/>
<location filename="mainscreen.py" line="118"/>
<source>Notifications</source>
<translation>Notifications</translation>
</message>
<message>
<location filename="mainscreen.py" line="108"/>
<location filename="mainscreen.py" line="119"/>
<source>Network</source>
<translation>Réseau</translation>
</message>
<message>
<location filename="mainscreen.py" line="109"/>
<location filename="mainscreen.py" line="120"/>
<source>About program</source>
<translation>À propos du programme</translation>
</message>
<message>
<location filename="profile.py" line="840"/>
<location filename="profile.py" line="854"/>
<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="842"/>
<location filename="profile.py" line="856"/>
<source>Friend request</source>
<translation>Demande d&apos;amis</translation>
</message>
@ -126,173 +132,289 @@
<translation type="obsolete">Toxygen est un client Tox écris en Python 2.7. Version : </translation>
</message>
<message>
<location filename="mainscreen.py" line="390"/>
<location filename="mainscreen.py" line="411"/>
<source>Choose file</source>
<translation>Choisir un fichier</translation>
</message>
<message>
<location filename="mainscreen.py" line="431"/>
<location filename="mainscreen.py" line="452"/>
<source>Disallow auto accept</source>
<translation>Désactiver l&apos;auto-réception</translation>
</message>
<message>
<location filename="mainscreen.py" line="432"/>
<location filename="mainscreen.py" line="453"/>
<source>Allow auto accept</source>
<translation>Activer l&apos;auto-réception</translation>
</message>
<message>
<location filename="mainscreen.py" line="434"/>
<location filename="mainscreen.py" line="455"/>
<source>Set alias</source>
<translation>Définir un alias</translation>
</message>
<message>
<location filename="mainscreen.py" line="435"/>
<location filename="mainscreen.py" line="456"/>
<source>Clear history</source>
<translation>Vider l&apos;historique</translation>
</message>
<message>
<location filename="mainscreen.py" line="436"/>
<location filename="mainscreen.py" line="457"/>
<source>Copy public key</source>
<translation>Copier la clé publique</translation>
</message>
<message>
<location filename="mainscreen.py" line="438"/>
<location filename="mainscreen.py" line="459"/>
<source>Remove friend</source>
<translation>Retirer un ami</translation>
</message>
<message>
<location filename="profile.py" line="686"/>
<location filename="profile.py" line="700"/>
<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="111"/>
<location filename="mainscreen.py" line="122"/>
<source>Audio</source>
<translation>Audio</translation>
</message>
<message>
<location filename="mainscreen.py" line="112"/>
<source>Find contact</source>
<translation>Trouver le contact</translation>
<translation type="obsolete">Trouver le contact</translation>
</message>
<message>
<location filename="profile.py" line="812"/>
<location filename="profile.py" line="826"/>
<source>Friend added</source>
<translation>Ami rajouté</translation>
</message>
<message>
<location filename="mainscreen.py" line="347"/>
<location filename="mainscreen.py" line="364"/>
<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="813"/>
<location filename="profile.py" line="827"/>
<source>Friend added without sending friend request</source>
<translation>Ami rajouté sans avoir envoyé de demande</translation>
</message>
<message>
<location filename="list_items.py" line="231"/>
<location filename="list_items.py" line="234"/>
<source>Choose folder</source>
<translation>Choisir le dossier</translation>
</message>
<message>
<location filename="mainscreen.py" line="113"/>
<location filename="mainscreen.py" line="124"/>
<source>Send screenshot</source>
<translation>Envoyer une capture d&apos;écran</translation>
</message>
<message>
<location filename="mainscreen.py" line="114"/>
<location filename="mainscreen.py" line="125"/>
<source>Send file</source>
<translation>Envoyer le fichier</translation>
</message>
<message>
<location filename="mainscreen.py" line="115"/>
<location filename="mainscreen.py" line="126"/>
<source>Send message</source>
<translation>Envoyer le message</translation>
</message>
<message>
<location filename="mainscreen.py" line="116"/>
<location filename="mainscreen.py" line="127"/>
<source>Start audio call with friend</source>
<translation>Lancer un appel audio avec un ami</translation>
</message>
<message>
<location filename="mainscreen.py" line="462"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="110"/>
<source>List of plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="123"/>
<source>Search</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="129"/>
<source>All</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainscreen.py" line="130"/>
<source>Online</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NetworkSettings</name>
<message>
<location filename="menu.py" line="230"/>
<location filename="menu.py" line="286"/>
<source>Network settings</source>
<translation>Paramètres réseaux</translation>
</message>
<message>
<location filename="menu.py" line="236"/>
<location filename="menu.py" line="292"/>
<source>Restart TOX core</source>
<translation>Relancer le noyau TOX</translation>
</message>
</context>
<context>
<name>PluginWindow</name>
<message>
<location filename="plugins/plugin_super_class.py" line="121"/>
<source>List of commands for plugin {}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="plugins/plugin_super_class.py" line="122"/>
<source>No commands available</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PluginsForm</name>
<message>
<location filename="menu.py" line="624"/>
<source>Plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="625"/>
<source>Open selected plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="637"/>
<source>No GUI found for this plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="651"/>
<source>No description available</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="663"/>
<source>Disable plugin</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="665"/>
<source>Enable plugin</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ProfileSettingsForm</name>
<message>
<location filename="menu.py" line="133"/>
<location filename="menu.py" line="161"/>
<source>Export profile</source>
<translation>Exporter le profile</translation>
</message>
<message>
<location filename="menu.py" line="134"/>
<location filename="menu.py" line="162"/>
<source>Profile settings</source>
<translation>Paramêtres du profil</translation>
</message>
<message>
<location filename="menu.py" line="135"/>
<location filename="menu.py" line="163"/>
<source>Name:</source>
<translation>Nom :</translation>
</message>
<message>
<location filename="menu.py" line="136"/>
<location filename="menu.py" line="164"/>
<source>Status:</source>
<translation>Status :</translation>
</message>
<message>
<location filename="menu.py" line="137"/>
<location filename="menu.py" line="165"/>
<source>TOX ID:</source>
<translation>ID TOX :</translation>
</message>
<message>
<location filename="menu.py" line="138"/>
<location filename="menu.py" line="166"/>
<source>Copy TOX ID</source>
<translation>Copier l&apos;ID TOX</translation>
</message>
<message>
<location filename="menu.py" line="139"/>
<location filename="menu.py" line="167"/>
<source>New avatar</source>
<translation>Nouvel avatar</translation>
</message>
<message>
<location filename="menu.py" line="140"/>
<location filename="menu.py" line="168"/>
<source>Reset avatar</source>
<translation>Réinitialiser l&apos;avatar</translation>
</message>
<message>
<location filename="menu.py" line="141"/>
<location filename="menu.py" line="169"/>
<source>New NoSpam</source>
<translation>Nouveau NoSpam</translation>
</message>
<message>
<location filename="menu.py" line="170"/>
<source>Profile password</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="171"/>
<source>Password (at least 8 symbols)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="172"/>
<source>Confirm password</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="173"/>
<source>Set password</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="191"/>
<source>Passwords do not match</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="175"/>
<source>Leaving blank will reset current password</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="176"/>
<source>There is no way to recover lost passwords</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="187"/>
<source>Password must be at least 8 symbols</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="menu.py" line="211"/>
<source>Choose avatar</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>audioSettingsForm</name>
<message>
<location filename="menu.py" line="522"/>
<location filename="menu.py" line="584"/>
<source>Audio settings</source>
<translation>Paramètres audio</translation>
</message>
<message>
<location filename="menu.py" line="523"/>
<location filename="menu.py" line="585"/>
<source>Input device:</source>
<translation>Péripherique d&apos;entrée :</translation>
</message>
<message>
<location filename="menu.py" line="524"/>
<location filename="menu.py" line="586"/>
<source>Output device:</source>
<translation>Péripherique de sortie :</translation>
</message>
@ -300,12 +422,12 @@ Version :</translation>
<context>
<name>incoming_call</name>
<message>
<location filename="profile.py" line="1151"/>
<location filename="profile.py" line="1168"/>
<source>Incoming video call</source>
<translation>Appel vidéo entrant</translation>
</message>
<message>
<location filename="profile.py" line="1153"/>
<location filename="profile.py" line="1170"/>
<source>Incoming audio call</source>
<translation>Appel audio entrant</translation>
</message>
@ -313,17 +435,17 @@ Version :</translation>
<context>
<name>interfaceForm</name>
<message>
<location filename="menu.py" line="460"/>
<location filename="menu.py" line="521"/>
<source>Interface settings</source>
<translation>Paramêtres de l&apos;interface</translation>
</message>
<message>
<location filename="menu.py" line="461"/>
<location filename="menu.py" line="522"/>
<source>Theme:</source>
<translation>Thème :</translation>
</message>
<message>
<location filename="menu.py" line="462"/>
<location filename="menu.py" line="523"/>
<source>Language:</source>
<translation>Langue :</translation>
</message>
@ -331,47 +453,47 @@ Version :</translation>
<context>
<name>login</name>
<message>
<location filename="loginscreen.py" line="70"/>
<location filename="loginscreen.py" line="74"/>
<source>Log in</source>
<translation>Se connecter</translation>
</message>
<message>
<location filename="loginscreen.py" line="71"/>
<location filename="loginscreen.py" line="75"/>
<source>Create</source>
<translation>Créer</translation>
</message>
<message>
<location filename="loginscreen.py" line="72"/>
<location filename="loginscreen.py" line="76"/>
<source>Profile name:</source>
<translation>Nom du profil :</translation>
</message>
<message>
<location filename="loginscreen.py" line="73"/>
<location filename="loginscreen.py" line="77"/>
<source>Load profile</source>
<translation>Charger le profil</translation>
</message>
<message>
<location filename="loginscreen.py" line="74"/>
<location filename="loginscreen.py" line="78"/>
<source>Use as default</source>
<translation>Utiliser par défaut</translation>
</message>
<message>
<location filename="loginscreen.py" line="75"/>
<location filename="loginscreen.py" line="79"/>
<source>Load existing profile</source>
<translation>Charger un profil existant</translation>
</message>
<message>
<location filename="loginscreen.py" line="76"/>
<location filename="loginscreen.py" line="80"/>
<source>Create new profile</source>
<translation>Créer un nouveau profil</translation>
</message>
<message>
<location filename="loginscreen.py" line="77"/>
<location filename="loginscreen.py" line="81"/>
<source>toxygen</source>
<translation>toxygen</translation>
</message>
<message>
<location filename="main.py" line="87"/>
<location filename="main.py" line="118"/>
<source>Looks like other instance of Toxygen uses this profile! Continue?</source>
<translation>Il semble qu&apos;une autre instance de Toxygen utilise ce profil ! Continuer ?</translation>
</message>
@ -379,22 +501,22 @@ Version :</translation>
<context>
<name>notificationsForm</name>
<message>
<location filename="menu.py" line="401"/>
<location filename="menu.py" line="461"/>
<source>Notification settings</source>
<translation>Paramêtres de notification</translation>
</message>
<message>
<location filename="menu.py" line="402"/>
<location filename="menu.py" line="462"/>
<source>Enable notifications</source>
<translation>Activer les notifications</translation>
</message>
<message>
<location filename="menu.py" line="403"/>
<location filename="menu.py" line="463"/>
<source>Enable call&apos;s sound</source>
<translation>Activer les sons d&apos;appel</translation>
</message>
<message>
<location filename="menu.py" line="404"/>
<location filename="menu.py" line="464"/>
<source>Enable sound notifications</source>
<translation>Activer les sons de notifications</translation>
</message>
@ -402,77 +524,77 @@ Version :</translation>
<context>
<name>privacySettings</name>
<message>
<location filename="menu.py" line="319"/>
<location filename="menu.py" line="378"/>
<source>Privacy settings</source>
<translation>Paramêtres de confidentialité</translation>
</message>
<message>
<location filename="menu.py" line="320"/>
<location filename="menu.py" line="379"/>
<source>Save chat history</source>
<translation>Sauvegarder l&apos;historique de chat</translation>
</message>
<message>
<location filename="menu.py" line="321"/>
<location filename="menu.py" line="380"/>
<source>Allow file auto accept</source>
<translation>Autoriser les fichier automatiquement</translation>
</message>
<message>
<location filename="menu.py" line="322"/>
<location filename="menu.py" line="381"/>
<source>Send typing notifications</source>
<translation>Notifier la frappe</translation>
</message>
<message>
<location filename="menu.py" line="323"/>
<location filename="menu.py" line="382"/>
<source>Auto accept default path:</source>
<translation>Chemin d&apos;accès des fichiers acceptés automatiquement :</translation>
</message>
<message>
<location filename="menu.py" line="324"/>
<location filename="menu.py" line="383"/>
<source>Change</source>
<translation>Modifier</translation>
</message>
<message>
<location filename="menu.py" line="325"/>
<location filename="menu.py" line="384"/>
<source>Allow inlines</source>
<translation>Activer l&apos;auto-réception</translation>
</message>
<message>
<location filename="menu.py" line="348"/>
<location filename="menu.py" line="407"/>
<source>Chat history</source>
<translation>Historique de chat</translation>
</message>
<message>
<location filename="menu.py" line="351"/>
<location filename="menu.py" line="410"/>
<source>History will be cleaned! Continue?</source>
<translation>L&apos;Historique va être nettoyé ! Confirmer ?</translation>
</message>
<message>
<location filename="menu.py" line="327"/>
<location filename="menu.py" line="386"/>
<source>Blocked users:</source>
<translation>Utilisateurs bloqués :</translation>
</message>
<message>
<location filename="menu.py" line="328"/>
<location filename="menu.py" line="387"/>
<source>Unblock</source>
<translation>Débloquer</translation>
</message>
<message>
<location filename="menu.py" line="329"/>
<location filename="menu.py" line="388"/>
<source>Block user</source>
<translation>Bloquer l&apos;utilisateur</translation>
</message>
<message>
<location filename="menu.py" line="335"/>
<location filename="menu.py" line="394"/>
<source>Add to friend list</source>
<translation>Ajouter à la liste des amis</translation>
</message>
<message>
<location filename="menu.py" line="336"/>
<location filename="menu.py" line="395"/>
<source>Do you want to add this user to friend list?</source>
<translation>Voulez vous rajouter cet utilisateur à votre liste d&apos;amis ?</translation>
</message>
<message>
<location filename="menu.py" line="326"/>
<location filename="menu.py" line="385"/>
<source>Block by TOX ID:</source>
<translation>Bloquer l&apos;ID TOX :</translation>
</message>
@ -480,14 +602,34 @@ Version :</translation>
<context>
<name>tray</name>
<message>
<location filename="main.py" line="113"/>
<location filename="main.py" line="166"/>
<source>Open Toxygen</source>
<translation>Ouvrir Toxygen</translation>
</message>
<message>
<location filename="main.py" line="114"/>
<location filename="main.py" line="175"/>
<source>Exit</source>
<translation>Quitter</translation>
</message>
<message>
<location filename="main.py" line="167"/>
<source>Set status</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="168"/>
<source>Online</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="169"/>
<source>Away</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="main.py" line="170"/>
<source>Busy</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

Binary file not shown.

View File

@ -4,17 +4,17 @@
<context>
<name>AddContact</name>
<message>
<location filename="menu.py" line="64"/>
<location filename="menu.py" line="69"/>
<source>Add contact</source>
<translation>Добавить контакт</translation>
</message>
<message>
<location filename="menu.py" line="66"/>
<location filename="menu.py" line="71"/>
<source>TOX ID:</source>
<translation>TOX ID:</translation>
</message>
<message>
<location filename="menu.py" line="67"/>
<location filename="menu.py" line="72"/>
<source>Message:</source>
<translation>Сообщение:</translation>
</message>
@ -22,239 +22,319 @@
<context>
<name>Form</name>
<message>
<location filename="menu.py" line="65"/>
<location filename="menu.py" line="70"/>
<source>Send request</source>
<translation>Отправить запрос</translation>
</message>
<message>
<location filename="menu.py" line="231"/>
<location filename="menu.py" line="287"/>
<source>IPv6</source>
<translation>IPv6</translation>
</message>
<message>
<location filename="menu.py" line="232"/>
<location filename="menu.py" line="288"/>
<source>UDP</source>
<translation>UDP</translation>
</message>
<message>
<location filename="menu.py" line="233"/>
<location filename="menu.py" line="289"/>
<source>Proxy</source>
<translation>Прокси</translation>
</message>
<message>
<location filename="menu.py" line="234"/>
<location filename="menu.py" line="290"/>
<source>IP:</source>
<translation>IP:</translation>
</message>
<message>
<location filename="menu.py" line="235"/>
<location filename="menu.py" line="291"/>
<source>Port:</source>
<translation>Порт:</translation>
</message>
<message>
<location filename="mainscreen.py" line="99"/>
<source>Online contacts</source>
<translation>Контакты в сети</translation>
<translation type="obsolete">Контакты в сети</translation>
</message>
<message>
<location filename="menu.py" line="237"/>
<location filename="menu.py" line="293"/>
<source>HTTP</source>
<translation>HTTP</translation>
</message>
<message>
<location filename="menu.py" line="295"/>
<source>WARNING:
using proxy with enabled UDP
can produce IP leak</source>
<translation type="unfinished">Предупреждение:
использование прокси с UDP
может привести к утечке IP</translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="mainscreen.py" line="104"/>
<location filename="mainscreen.py" line="115"/>
<source>Profile</source>
<translation>Профиль</translation>
</message>
<message>
<location filename="mainscreen.py" line="110"/>
<location filename="mainscreen.py" line="121"/>
<source>Settings</source>
<translation>Настройки</translation>
</message>
<message>
<location filename="mainscreen.py" line="346"/>
<location filename="mainscreen.py" line="363"/>
<source>About</source>
<translation>О программе</translation>
</message>
<message>
<location filename="mainscreen.py" line="103"/>
<location filename="mainscreen.py" line="114"/>
<source>Add contact</source>
<translation>Добавить контакт</translation>
</message>
<message>
<location filename="mainscreen.py" line="105"/>
<location filename="mainscreen.py" line="116"/>
<source>Privacy</source>
<translation>Приватность</translation>
</message>
<message>
<location filename="mainscreen.py" line="106"/>
<location filename="mainscreen.py" line="117"/>
<source>Interface</source>
<translation>Интерфейс</translation>
</message>
<message>
<location filename="mainscreen.py" line="107"/>
<location filename="mainscreen.py" line="118"/>
<source>Notifications</source>
<translation>Уведомления</translation>
</message>
<message>
<location filename="mainscreen.py" line="108"/>
<location filename="mainscreen.py" line="119"/>
<source>Network</source>
<translation>Сеть</translation>
</message>
<message>
<location filename="mainscreen.py" line="109"/>
<location filename="mainscreen.py" line="120"/>
<source>About program</source>
<translation>О программе</translation>
</message>
<message>
<location filename="profile.py" line="840"/>
<location filename="profile.py" line="854"/>
<source>User {} wants to add you to contact list. Message:
{}</source>
<translation>Пользователь {} хочет добавить Вас в список контактов. Сообщение:
{}</translation>
</message>
<message>
<location filename="profile.py" line="842"/>
<location filename="profile.py" line="856"/>
<source>Friend request</source>
<translation>Запрос на добавление в друзья</translation>
</message>
<message>
<location filename="mainscreen.py" line="390"/>
<location filename="mainscreen.py" line="411"/>
<source>Choose file</source>
<translation>Выберите файл</translation>
</message>
<message>
<location filename="mainscreen.py" line="431"/>
<location filename="mainscreen.py" line="452"/>
<source>Disallow auto accept</source>
<translation>Запретить автоматическое получение файлов</translation>
</message>
<message>
<location filename="mainscreen.py" line="432"/>
<location filename="mainscreen.py" line="453"/>
<source>Allow auto accept</source>
<translation>Разрешить автоматическое получение файлов</translation>
</message>
<message>
<location filename="mainscreen.py" line="434"/>
<location filename="mainscreen.py" line="455"/>
<source>Set alias</source>
<translation>Изменить псевдоним</translation>
</message>
<message>
<location filename="mainscreen.py" line="435"/>
<location filename="mainscreen.py" line="456"/>
<source>Clear history</source>
<translation>Очистить историю</translation>
</message>
<message>
<location filename="mainscreen.py" line="436"/>
<location filename="mainscreen.py" line="457"/>
<source>Copy public key</source>
<translation>Копировать публичный ключ</translation>
</message>
<message>
<location filename="mainscreen.py" line="438"/>
<location filename="mainscreen.py" line="459"/>
<source>Remove friend</source>
<translation>Удалить друга</translation>
</message>
<message>
<location filename="profile.py" line="686"/>
<location filename="profile.py" line="700"/>
<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="111"/>
<location filename="mainscreen.py" line="122"/>
<source>Audio</source>
<translation>Аудио</translation>
</message>
<message>
<location filename="mainscreen.py" line="112"/>
<source>Find contact</source>
<translation>Найти контакт</translation>
<translation type="obsolete">Найти контакт</translation>
</message>
<message>
<location filename="profile.py" line="812"/>
<location filename="profile.py" line="826"/>
<source>Friend added</source>
<translation>Друг добавлен</translation>
</message>
<message>
<location filename="mainscreen.py" line="347"/>
<location filename="mainscreen.py" line="364"/>
<source>Toxygen is Tox client written on Python.
Version: </source>
<translation>Toxygen - клиент для мессенджера Tox, написанный на Python. Версия: </translation>
</message>
<message>
<location filename="profile.py" line="813"/>
<location filename="profile.py" line="827"/>
<source>Friend added without sending friend request</source>
<translation>Друг добавлен без отправки запроса на добавление в друзья</translation>
</message>
<message>
<location filename="list_items.py" line="231"/>
<location filename="list_items.py" line="234"/>
<source>Choose folder</source>
<translation>Выбрать папку</translation>
</message>
<message>
<location filename="mainscreen.py" line="113"/>
<location filename="mainscreen.py" line="124"/>
<source>Send screenshot</source>
<translation>Отправить снимок экрана</translation>
</message>
<message>
<location filename="mainscreen.py" line="114"/>
<location filename="mainscreen.py" line="125"/>
<source>Send file</source>
<translation>Отправить файл</translation>
</message>
<message>
<location filename="mainscreen.py" line="115"/>
<location filename="mainscreen.py" line="126"/>
<source>Send message</source>
<translation>Отправить сообщение</translation>
</message>
<message>
<location filename="mainscreen.py" line="116"/>
<location filename="mainscreen.py" line="127"/>
<source>Start audio call with friend</source>
<translation>Начать аудиозвонок с другом</translation>
</message>
<message>
<location filename="mainscreen.py" line="462"/>
<source>Plugins</source>
<translation>Плагины</translation>
</message>
<message>
<location filename="mainscreen.py" line="110"/>
<source>List of plugins</source>
<translation>Список плагинов</translation>
</message>
<message>
<location filename="mainscreen.py" line="123"/>
<source>Search</source>
<translation>Поиск</translation>
</message>
<message>
<location filename="mainscreen.py" line="129"/>
<source>All</source>
<translation>Все</translation>
</message>
<message>
<location filename="mainscreen.py" line="130"/>
<source>Online</source>
<translation>Онлайн</translation>
</message>
</context>
<context>
<name>NetworkSettings</name>
<message>
<location filename="menu.py" line="230"/>
<location filename="menu.py" line="286"/>
<source>Network settings</source>
<translation>Настройки сети</translation>
</message>
<message>
<location filename="menu.py" line="236"/>
<location filename="menu.py" line="292"/>
<source>Restart TOX core</source>
<translation>Перезапустить ядро TOX</translation>
</message>
</context>
<context>
<name>PluginWindow</name>
<message>
<location filename="plugins/plugin_super_class.py" line="121"/>
<source>List of commands for plugin {}</source>
<translation>Список команд для плагина {}</translation>
</message>
<message>
<location filename="plugins/plugin_super_class.py" line="122"/>
<source>No commands available</source>
<translation>Команды не найдены</translation>
</message>
</context>
<context>
<name>PluginsForm</name>
<message>
<location filename="menu.py" line="624"/>
<source>Plugins</source>
<translation>Плагины</translation>
</message>
<message>
<location filename="menu.py" line="625"/>
<source>Open selected plugin</source>
<translation>Открыть выбранный плагин</translation>
</message>
<message>
<location filename="menu.py" line="637"/>
<source>No GUI found for this plugin</source>
<translation>GUI для данного плагина не найден</translation>
</message>
<message>
<location filename="menu.py" line="651"/>
<source>No description available</source>
<translation>Описание недоступно</translation>
</message>
<message>
<location filename="menu.py" line="663"/>
<source>Disable plugin</source>
<translation>Отключить плагин</translation>
</message>
<message>
<location filename="menu.py" line="665"/>
<source>Enable plugin</source>
<translation>Включить плагин</translation>
</message>
</context>
<context>
<name>ProfileSettingsForm</name>
<message>
<location filename="menu.py" line="133"/>
<location filename="menu.py" line="161"/>
<source>Export profile</source>
<translation>Экспорт профиля</translation>
</message>
<message>
<location filename="menu.py" line="134"/>
<location filename="menu.py" line="162"/>
<source>Profile settings</source>
<translation>Настройки профиля</translation>
</message>
<message>
<location filename="menu.py" line="135"/>
<location filename="menu.py" line="163"/>
<source>Name:</source>
<translation>Имя:</translation>
</message>
<message>
<location filename="menu.py" line="136"/>
<location filename="menu.py" line="164"/>
<source>Status:</source>
<translation>Статус:</translation>
</message>
<message>
<location filename="menu.py" line="137"/>
<location filename="menu.py" line="165"/>
<source>TOX ID:</source>
<translation>TOX ID:</translation>
</message>
<message>
<location filename="menu.py" line="138"/>
<location filename="menu.py" line="166"/>
<source>Copy TOX ID</source>
<translation>Копировать TOX ID</translation>
</message>
@ -264,35 +344,80 @@ Version: </source>
<translation type="obsolete">Язык:</translation>
</message>
<message>
<location filename="menu.py" line="139"/>
<location filename="menu.py" line="167"/>
<source>New avatar</source>
<translation>Новый аватар</translation>
</message>
<message>
<location filename="menu.py" line="140"/>
<location filename="menu.py" line="168"/>
<source>Reset avatar</source>
<translation>Сбросить аватар</translation>
</message>
<message>
<location filename="menu.py" line="141"/>
<location filename="menu.py" line="169"/>
<source>New NoSpam</source>
<translation>Новый NoSpam</translation>
</message>
<message>
<location filename="menu.py" line="170"/>
<source>Profile password</source>
<translation>Пароль профиля</translation>
</message>
<message>
<location filename="menu.py" line="171"/>
<source>Password (at least 8 symbols)</source>
<translation>Пароль (минимум 8 символов)</translation>
</message>
<message>
<location filename="menu.py" line="172"/>
<source>Confirm password</source>
<translation>Подтверждение пароля</translation>
</message>
<message>
<location filename="menu.py" line="173"/>
<source>Set password</source>
<translation>Изменить пароль</translation>
</message>
<message>
<location filename="menu.py" line="191"/>
<source>Passwords do not match</source>
<translation>Пароли не совпадают</translation>
</message>
<message>
<location filename="menu.py" line="175"/>
<source>Leaving blank will reset current password</source>
<translation>Пустое поле сбросит текущий пароль</translation>
</message>
<message>
<location filename="menu.py" line="176"/>
<source>There is no way to recover lost passwords</source>
<translation>Восстановление забытых паролей не поддерживается</translation>
</message>
<message>
<location filename="menu.py" line="187"/>
<source>Password must be at least 8 symbols</source>
<translation>Пароль должен быть длиной не менее 8 символов</translation>
</message>
<message>
<location filename="menu.py" line="211"/>
<source>Choose avatar</source>
<translation>Выбрать аватар</translation>
</message>
</context>
<context>
<name>audioSettingsForm</name>
<message>
<location filename="menu.py" line="522"/>
<location filename="menu.py" line="584"/>
<source>Audio settings</source>
<translation>Настройки аудио</translation>
</message>
<message>
<location filename="menu.py" line="523"/>
<location filename="menu.py" line="585"/>
<source>Input device:</source>
<translation>Устройство ввода:</translation>
</message>
<message>
<location filename="menu.py" line="524"/>
<location filename="menu.py" line="586"/>
<source>Output device:</source>
<translation>Устройство вывода:</translation>
</message>
@ -300,12 +425,12 @@ Version: </source>
<context>
<name>incoming_call</name>
<message>
<location filename="profile.py" line="1151"/>
<location filename="profile.py" line="1168"/>
<source>Incoming video call</source>
<translation>Входящий видеозвонок</translation>
</message>
<message>
<location filename="profile.py" line="1153"/>
<location filename="profile.py" line="1170"/>
<source>Incoming audio call</source>
<translation>Входящий аудиозвонок</translation>
</message>
@ -313,17 +438,17 @@ Version: </source>
<context>
<name>interfaceForm</name>
<message>
<location filename="menu.py" line="460"/>
<location filename="menu.py" line="521"/>
<source>Interface settings</source>
<translation>Настройки интерфейса</translation>
</message>
<message>
<location filename="menu.py" line="461"/>
<location filename="menu.py" line="522"/>
<source>Theme:</source>
<translation>Тема:</translation>
</message>
<message>
<location filename="menu.py" line="462"/>
<location filename="menu.py" line="523"/>
<source>Language:</source>
<translation>Язык:</translation>
</message>
@ -331,47 +456,47 @@ Version: </source>
<context>
<name>login</name>
<message>
<location filename="loginscreen.py" line="70"/>
<location filename="loginscreen.py" line="74"/>
<source>Log in</source>
<translation>Вход</translation>
</message>
<message>
<location filename="loginscreen.py" line="71"/>
<location filename="loginscreen.py" line="75"/>
<source>Create</source>
<translation>Создать</translation>
</message>
<message>
<location filename="loginscreen.py" line="72"/>
<location filename="loginscreen.py" line="76"/>
<source>Profile name:</source>
<translation>Имя профиля:</translation>
</message>
<message>
<location filename="loginscreen.py" line="73"/>
<location filename="loginscreen.py" line="77"/>
<source>Load profile</source>
<translation>Загрузить профиль</translation>
</message>
<message>
<location filename="loginscreen.py" line="74"/>
<location filename="loginscreen.py" line="78"/>
<source>Use as default</source>
<translation>По умолчанию</translation>
</message>
<message>
<location filename="loginscreen.py" line="75"/>
<location filename="loginscreen.py" line="79"/>
<source>Load existing profile</source>
<translation>Загрузить профиль</translation>
</message>
<message>
<location filename="loginscreen.py" line="76"/>
<location filename="loginscreen.py" line="80"/>
<source>Create new profile</source>
<translation>Создать новый профиль</translation>
</message>
<message>
<location filename="loginscreen.py" line="77"/>
<location filename="loginscreen.py" line="81"/>
<source>toxygen</source>
<translation>toxygen</translation>
</message>
<message>
<location filename="main.py" line="87"/>
<location filename="main.py" line="118"/>
<source>Looks like other instance of Toxygen uses this profile! Continue?</source>
<translation>Похоже, что этот профиль используется другим экземпляром Toxygen! Продолжить?</translation>
</message>
@ -379,22 +504,22 @@ Version: </source>
<context>
<name>notificationsForm</name>
<message>
<location filename="menu.py" line="401"/>
<location filename="menu.py" line="461"/>
<source>Notification settings</source>
<translation>Настройки уведомлений</translation>
</message>
<message>
<location filename="menu.py" line="402"/>
<location filename="menu.py" line="462"/>
<source>Enable notifications</source>
<translation>Включить уведомления</translation>
</message>
<message>
<location filename="menu.py" line="403"/>
<location filename="menu.py" line="463"/>
<source>Enable call&apos;s sound</source>
<translation>Включить звук звонка</translation>
</message>
<message>
<location filename="menu.py" line="404"/>
<location filename="menu.py" line="464"/>
<source>Enable sound notifications</source>
<translation>Включить звуковые уведомления
</translation>
@ -403,77 +528,77 @@ Version: </source>
<context>
<name>privacySettings</name>
<message>
<location filename="menu.py" line="319"/>
<location filename="menu.py" line="378"/>
<source>Privacy settings</source>
<translation>Настройки приватности</translation>
</message>
<message>
<location filename="menu.py" line="320"/>
<location filename="menu.py" line="379"/>
<source>Save chat history</source>
<translation>Сохранять историю переписки</translation>
</message>
<message>
<location filename="menu.py" line="321"/>
<location filename="menu.py" line="380"/>
<source>Allow file auto accept</source>
<translation>Разрешить автополучение файлов</translation>
</message>
<message>
<location filename="menu.py" line="322"/>
<location filename="menu.py" line="381"/>
<source>Send typing notifications</source>
<translation>Посылать уведомления о наборе текста</translation>
</message>
<message>
<location filename="menu.py" line="323"/>
<location filename="menu.py" line="382"/>
<source>Auto accept default path:</source>
<translation>Путь автоприема файлов:</translation>
</message>
<message>
<location filename="menu.py" line="324"/>
<location filename="menu.py" line="383"/>
<source>Change</source>
<translation>Изменить</translation>
</message>
<message>
<location filename="menu.py" line="325"/>
<location filename="menu.py" line="384"/>
<source>Allow inlines</source>
<translation>Разрешать инлайны</translation>
</message>
<message>
<location filename="menu.py" line="348"/>
<location filename="menu.py" line="407"/>
<source>Chat history</source>
<translation>История чата</translation>
</message>
<message>
<location filename="menu.py" line="351"/>
<location filename="menu.py" line="410"/>
<source>History will be cleaned! Continue?</source>
<translation>История переписки будет очищена! Продолжить?</translation>
</message>
<message>
<location filename="menu.py" line="327"/>
<location filename="menu.py" line="386"/>
<source>Blocked users:</source>
<translation>Заблокированные пользователи:</translation>
</message>
<message>
<location filename="menu.py" line="328"/>
<location filename="menu.py" line="387"/>
<source>Unblock</source>
<translation>Разблокировать</translation>
</message>
<message>
<location filename="menu.py" line="329"/>
<location filename="menu.py" line="388"/>
<source>Block user</source>
<translation>Заблокировать пользователя</translation>
</message>
<message>
<location filename="menu.py" line="335"/>
<location filename="menu.py" line="394"/>
<source>Add to friend list</source>
<translation>Добавить в список друзей</translation>
</message>
<message>
<location filename="menu.py" line="336"/>
<location filename="menu.py" line="395"/>
<source>Do you want to add this user to friend list?</source>
<translation>Добавить этого пользователя в список друзей?</translation>
</message>
<message>
<location filename="menu.py" line="326"/>
<location filename="menu.py" line="385"/>
<source>Block by TOX ID:</source>
<translation>Блокировать по TOX ID:</translation>
</message>
@ -481,14 +606,34 @@ Version: </source>
<context>
<name>tray</name>
<message>
<location filename="main.py" line="113"/>
<location filename="main.py" line="166"/>
<source>Open Toxygen</source>
<translation>Открыть Toxygen</translation>
</message>
<message>
<location filename="main.py" line="114"/>
<location filename="main.py" line="175"/>
<source>Exit</source>
<translation>Выход</translation>
</message>
<message>
<location filename="main.py" line="167"/>
<source>Set status</source>
<translation>Изменить статус</translation>
</message>
<message>
<location filename="main.py" line="168"/>
<source>Online</source>
<translation>Онлайн</translation>
</message>
<message>
<location filename="main.py" line="169"/>
<source>Away</source>
<translation>Нет на месте</translation>
</message>
<message>
<location filename="main.py" line="170"/>
<source>Busy</source>
<translation>Занят</translation>
</message>
</context>
</TS>

View File

@ -3,7 +3,7 @@ import time
from platform import system
program_version = '0.1.2'
program_version = '0.1.3'
def log(data):

View File

@ -1,4 +1,7 @@
from PySide import QtGui, QtCore
try:
from PySide import QtCore, QtGui
except ImportError:
from PyQt4 import QtCore, QtGui
class DataLabel(QtGui.QLabel):
@ -21,3 +24,14 @@ class CenteredWidget(QtGui.QWidget):
cp = QtGui.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
class QRightClickButton(QtGui.QPushButton):
def __init__(self, parent):
super(QRightClickButton, self).__init__(parent)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.RightButton:
self.emit(QtCore.SIGNAL("rightClicked()"))
else:
super(QRightClickButton, self).mousePressEvent(event)

View File

@ -2,6 +2,7 @@ from src.bootstrap import node_generator
from src.profile import *
from src.settings import ProfileHelper
from src.tox_dns import tox_dns
from src.toxencryptsave import LibToxEncryptSave
class TestProfile():
@ -12,13 +13,13 @@ class TestProfile():
assert len(arr) >= 2
def test_open(self):
data = ProfileHelper.open_profile(Settings.get_default_path(), 'alice')
data = ProfileHelper(Settings.get_default_path(), 'alice').open_profile()
assert data
def test_open_save(self):
data = ProfileHelper.open_profile(Settings.get_default_path(), 'alice')
ProfileHelper.save_profile(data)
new_data = ProfileHelper.open_profile(Settings.get_default_path(), 'alice')
data = ProfileHelper(Settings.get_default_path(), 'alice').open_profile()
ProfileHelper.get_instance().save_profile(data)
new_data = ProfileHelper(Settings.get_default_path(), 'alice').open_profile()
assert new_data == data
@ -36,7 +37,7 @@ class TestNodeGen():
class TestTox():
def test_loading(self):
data = ProfileHelper.open_profile(Settings.get_default_path(), 'alice')
data = ProfileHelper(Settings.get_default_path(), 'alice').open_profile()
settings = Settings.get_default_settings()
tox = tox_factory(data, settings)
for data in node_generator():
@ -56,13 +57,13 @@ class TestTox():
assert tox.self_get_status_message() == status_message
def test_friend_list(self):
data = ProfileHelper.open_profile(Settings.get_default_path(), 'bob')
data = ProfileHelper(Settings.get_default_path(), 'bob').open_profile()
settings = Settings.get_default_settings()
tox = tox_factory(data, settings)
s = tox.self_get_friend_list()
size = tox.self_get_friend_list_size()
assert size == 2
assert len(s) == 2
assert size <= 2
assert len(s) <= 2
del tox
@ -72,3 +73,15 @@ class TestDNS():
bot_id = '56A1ADE4B65B86BCD51CC73E2CD4E542179F47959FE3E0E21B4B0ACDADE51855D34D34D37CB5'
tox_id = tox_dns('groupbot@toxme.io')
assert tox_id == bot_id
class TestEncryption():
def test_encr_decr(self):
with open(settings.Settings.get_default_path() + '/alice.tox') as fl:
data = fl.read()
lib = LibToxEncryptSave('easypassword')
copy_data = data[:]
data = lib.pass_encrypt(data)
data = lib.pass_decrypt(data)
assert copy_data == data