merge in next_gen branch
This commit is contained in:
parent
b51ec9bd71
commit
6f0c1a444e
37 changed files with 0 additions and 10499 deletions
|
@ -1,134 +0,0 @@
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
||||||
import widgets
|
|
||||||
import profile
|
|
||||||
import util
|
|
||||||
import pyaudio
|
|
||||||
import wave
|
|
||||||
import settings
|
|
||||||
from util import curr_directory
|
|
||||||
|
|
||||||
|
|
||||||
class IncomingCallWidget(widgets.CenteredWidget):
|
|
||||||
|
|
||||||
def __init__(self, friend_number, text, name):
|
|
||||||
super(IncomingCallWidget, self).__init__()
|
|
||||||
self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowStaysOnTopHint)
|
|
||||||
self.resize(QtCore.QSize(500, 270))
|
|
||||||
self.avatar_label = QtWidgets.QLabel(self)
|
|
||||||
self.avatar_label.setGeometry(QtCore.QRect(10, 20, 64, 64))
|
|
||||||
self.avatar_label.setScaledContents(False)
|
|
||||||
self.name = widgets.DataLabel(self)
|
|
||||||
self.name.setGeometry(QtCore.QRect(90, 20, 300, 25))
|
|
||||||
self._friend_number = friend_number
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setFamily(settings.Settings.get_instance()['font'])
|
|
||||||
font.setPointSize(16)
|
|
||||||
font.setBold(True)
|
|
||||||
self.name.setFont(font)
|
|
||||||
self.call_type = widgets.DataLabel(self)
|
|
||||||
self.call_type.setGeometry(QtCore.QRect(90, 55, 300, 25))
|
|
||||||
self.call_type.setFont(font)
|
|
||||||
self.accept_audio = QtWidgets.QPushButton(self)
|
|
||||||
self.accept_audio.setGeometry(QtCore.QRect(20, 100, 150, 150))
|
|
||||||
self.accept_video = QtWidgets.QPushButton(self)
|
|
||||||
self.accept_video.setGeometry(QtCore.QRect(170, 100, 150, 150))
|
|
||||||
self.decline = QtWidgets.QPushButton(self)
|
|
||||||
self.decline.setGeometry(QtCore.QRect(320, 100, 150, 150))
|
|
||||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/accept_audio.png')
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.accept_audio.setIcon(icon)
|
|
||||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/accept_video.png')
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.accept_video.setIcon(icon)
|
|
||||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/decline_call.png')
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.decline.setIcon(icon)
|
|
||||||
self.accept_audio.setIconSize(QtCore.QSize(150, 150))
|
|
||||||
self.accept_video.setIconSize(QtCore.QSize(140, 140))
|
|
||||||
self.decline.setIconSize(QtCore.QSize(140, 140))
|
|
||||||
self.accept_audio.setStyleSheet("QPushButton { border: none }")
|
|
||||||
self.accept_video.setStyleSheet("QPushButton { border: none }")
|
|
||||||
self.decline.setStyleSheet("QPushButton { border: none }")
|
|
||||||
self.setWindowTitle(text)
|
|
||||||
self.name.setText(name)
|
|
||||||
self.call_type.setText(text)
|
|
||||||
self._processing = False
|
|
||||||
self.accept_audio.clicked.connect(self.accept_call_with_audio)
|
|
||||||
self.accept_video.clicked.connect(self.accept_call_with_video)
|
|
||||||
self.decline.clicked.connect(self.decline_call)
|
|
||||||
|
|
||||||
class SoundPlay(QtCore.QThread):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
QtCore.QThread.__init__(self)
|
|
||||||
self.a = None
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
class AudioFile:
|
|
||||||
chunk = 1024
|
|
||||||
|
|
||||||
def __init__(self, fl):
|
|
||||||
self.stop = False
|
|
||||||
self.fl = fl
|
|
||||||
self.wf = wave.open(self.fl, 'rb')
|
|
||||||
self.p = pyaudio.PyAudio()
|
|
||||||
self.stream = self.p.open(
|
|
||||||
format=self.p.get_format_from_width(self.wf.getsampwidth()),
|
|
||||||
channels=self.wf.getnchannels(),
|
|
||||||
rate=self.wf.getframerate(),
|
|
||||||
output=True)
|
|
||||||
|
|
||||||
def play(self):
|
|
||||||
while not self.stop:
|
|
||||||
data = self.wf.readframes(self.chunk)
|
|
||||||
while data and not self.stop:
|
|
||||||
self.stream.write(data)
|
|
||||||
data = self.wf.readframes(self.chunk)
|
|
||||||
self.wf = wave.open(self.fl, 'rb')
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.stream.close()
|
|
||||||
self.p.terminate()
|
|
||||||
|
|
||||||
self.a = AudioFile(curr_directory() + '/sounds/call.wav')
|
|
||||||
self.a.play()
|
|
||||||
self.a.close()
|
|
||||||
|
|
||||||
if settings.Settings.get_instance()['calls_sound']:
|
|
||||||
self.thread = SoundPlay()
|
|
||||||
self.thread.start()
|
|
||||||
else:
|
|
||||||
self.thread = None
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
if self.thread is not None:
|
|
||||||
self.thread.a.stop = True
|
|
||||||
self.thread.wait()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def accept_call_with_audio(self):
|
|
||||||
if self._processing:
|
|
||||||
return
|
|
||||||
self._processing = True
|
|
||||||
pr = profile.Profile.get_instance()
|
|
||||||
pr.accept_call(self._friend_number, True, False)
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def accept_call_with_video(self):
|
|
||||||
if self._processing:
|
|
||||||
return
|
|
||||||
self._processing = True
|
|
||||||
pr = profile.Profile.get_instance()
|
|
||||||
pr.accept_call(self._friend_number, True, True)
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def decline_call(self):
|
|
||||||
if self._processing:
|
|
||||||
return
|
|
||||||
self._processing = True
|
|
||||||
pr = profile.Profile.get_instance()
|
|
||||||
pr.stop_call(self._friend_number, False)
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def set_pixmap(self, pixmap):
|
|
||||||
self.avatar_label.setPixmap(pixmap)
|
|
|
@ -1,118 +0,0 @@
|
||||||
from settings import *
|
|
||||||
from PyQt5 import QtCore, QtGui
|
|
||||||
from toxcore_enums_and_consts import TOX_PUBLIC_KEY_SIZE
|
|
||||||
|
|
||||||
|
|
||||||
class BaseContact:
|
|
||||||
"""
|
|
||||||
Class encapsulating TOX contact
|
|
||||||
Properties: name (alias of contact or name), status_message, status (connection status)
|
|
||||||
widget - widget for update, tox id (or public key)
|
|
||||||
Base class for all contacts.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name, status_message, widget, tox_id):
|
|
||||||
"""
|
|
||||||
:param name: name, example: 'Toxygen user'
|
|
||||||
:param status_message: status message, example: 'Toxing on Toxygen'
|
|
||||||
:param widget: ContactItem instance
|
|
||||||
:param tox_id: tox id of contact
|
|
||||||
"""
|
|
||||||
self._name, self._status_message = name, status_message
|
|
||||||
self._status, self._widget = None, widget
|
|
||||||
self._tox_id = tox_id
|
|
||||||
self.init_widget()
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Name - current name or alias of user
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
def set_name(self, value):
|
|
||||||
self._name = str(value, 'utf-8')
|
|
||||||
self._widget.name.setText(self._name)
|
|
||||||
self._widget.name.repaint()
|
|
||||||
|
|
||||||
name = property(get_name, set_name)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Status message
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_status_message(self):
|
|
||||||
return self._status_message
|
|
||||||
|
|
||||||
def set_status_message(self, value):
|
|
||||||
self._status_message = str(value, 'utf-8')
|
|
||||||
self._widget.status_message.setText(self._status_message)
|
|
||||||
self._widget.status_message.repaint()
|
|
||||||
|
|
||||||
status_message = property(get_status_message, set_status_message)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Status
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_status(self):
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
def set_status(self, value):
|
|
||||||
self._status = value
|
|
||||||
self._widget.connection_status.update(value)
|
|
||||||
|
|
||||||
status = property(get_status, set_status)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# TOX ID. WARNING: for friend it will return public key, for profile - full address
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_tox_id(self):
|
|
||||||
return self._tox_id
|
|
||||||
|
|
||||||
tox_id = property(get_tox_id)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Avatars
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def load_avatar(self):
|
|
||||||
"""
|
|
||||||
Tries to load avatar of contact or uses default avatar
|
|
||||||
"""
|
|
||||||
prefix = ProfileHelper.get_path() + 'avatars/'
|
|
||||||
avatar_path = prefix + '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
|
|
||||||
if not os.path.isfile(avatar_path) or not os.path.getsize(avatar_path): # load default image
|
|
||||||
avatar_path = curr_directory() + '/images/avatar.png'
|
|
||||||
width = self._widget.avatar_label.width()
|
|
||||||
pixmap = QtGui.QPixmap(avatar_path)
|
|
||||||
self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio,
|
|
||||||
QtCore.Qt.SmoothTransformation))
|
|
||||||
self._widget.avatar_label.repaint()
|
|
||||||
|
|
||||||
def reset_avatar(self):
|
|
||||||
avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
|
|
||||||
if os.path.isfile(avatar_path):
|
|
||||||
os.remove(avatar_path)
|
|
||||||
self.load_avatar()
|
|
||||||
|
|
||||||
def set_avatar(self, avatar):
|
|
||||||
avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
|
|
||||||
with open(avatar_path, 'wb') as f:
|
|
||||||
f.write(avatar)
|
|
||||||
self.load_avatar()
|
|
||||||
|
|
||||||
def get_pixmap(self):
|
|
||||||
return self._widget.avatar_label.pixmap()
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Widgets
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def init_widget(self):
|
|
||||||
if self._widget is not None:
|
|
||||||
self._widget.name.setText(self._name)
|
|
||||||
self._widget.status_message.setText(self._status_message)
|
|
||||||
self._widget.connection_status.update(self._status)
|
|
||||||
self.load_avatar()
|
|
|
@ -1,75 +0,0 @@
|
||||||
import random
|
|
||||||
import urllib.request
|
|
||||||
from util import log, curr_directory
|
|
||||||
import settings
|
|
||||||
from PyQt5 import QtNetwork, QtCore
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class Node:
|
|
||||||
|
|
||||||
def __init__(self, node):
|
|
||||||
self._ip, self._port, self._tox_key = node['ipv4'], node['port'], node['public_key']
|
|
||||||
self._priority = random.randint(1, 1000000) if node['status_tcp'] and node['status_udp'] else 0
|
|
||||||
|
|
||||||
def get_priority(self):
|
|
||||||
return self._priority
|
|
||||||
|
|
||||||
priority = property(get_priority)
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return bytes(self._ip, 'utf-8'), self._port, self._tox_key
|
|
||||||
|
|
||||||
|
|
||||||
def generate_nodes():
|
|
||||||
with open(curr_directory() + '/nodes.json', 'rt') as fl:
|
|
||||||
json_nodes = json.loads(fl.read())['nodes']
|
|
||||||
nodes = map(lambda json_node: Node(json_node), json_nodes)
|
|
||||||
sorted_nodes = sorted(nodes, key=lambda x: x.priority)[-4:]
|
|
||||||
for node in sorted_nodes:
|
|
||||||
yield node.get_data()
|
|
||||||
|
|
||||||
|
|
||||||
def save_nodes(nodes):
|
|
||||||
if not nodes:
|
|
||||||
return
|
|
||||||
print('Saving nodes...')
|
|
||||||
with open(curr_directory() + '/nodes.json', 'wb') as fl:
|
|
||||||
fl.write(nodes)
|
|
||||||
|
|
||||||
|
|
||||||
def download_nodes_list():
|
|
||||||
url = 'https://nodes.tox.chat/json'
|
|
||||||
s = settings.Settings.get_instance()
|
|
||||||
if not s['download_nodes_list']:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not s['proxy_type']: # no proxy
|
|
||||||
try:
|
|
||||||
req = urllib.request.Request(url)
|
|
||||||
req.add_header('Content-Type', 'application/json')
|
|
||||||
response = urllib.request.urlopen(req)
|
|
||||||
result = response.read()
|
|
||||||
save_nodes(result)
|
|
||||||
except Exception as ex:
|
|
||||||
log('TOX nodes loading error: ' + str(ex))
|
|
||||||
else: # proxy
|
|
||||||
netman = QtNetwork.QNetworkAccessManager()
|
|
||||||
proxy = QtNetwork.QNetworkProxy()
|
|
||||||
proxy.setType(
|
|
||||||
QtNetwork.QNetworkProxy.Socks5Proxy if s['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy)
|
|
||||||
proxy.setHostName(s['proxy_host'])
|
|
||||||
proxy.setPort(s['proxy_port'])
|
|
||||||
netman.setProxy(proxy)
|
|
||||||
try:
|
|
||||||
request = QtNetwork.QNetworkRequest()
|
|
||||||
request.setUrl(QtCore.QUrl(url))
|
|
||||||
reply = netman.get(request)
|
|
||||||
|
|
||||||
while not reply.isFinished():
|
|
||||||
QtCore.QThread.msleep(1)
|
|
||||||
QtCore.QCoreApplication.processEvents()
|
|
||||||
data = bytes(reply.readAll().data())
|
|
||||||
save_nodes(data)
|
|
||||||
except Exception as ex:
|
|
||||||
log('TOX nodes loading error: ' + str(ex))
|
|
|
@ -1,469 +0,0 @@
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
||||||
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 plugin_support import PluginLoader
|
|
||||||
import queue
|
|
||||||
import threading
|
|
||||||
import util
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Threads
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class InvokeEvent(QtCore.QEvent):
|
|
||||||
EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType())
|
|
||||||
|
|
||||||
def __init__(self, fn, *args, **kwargs):
|
|
||||||
QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE)
|
|
||||||
self.fn = fn
|
|
||||||
self.args = args
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
|
|
||||||
class Invoker(QtCore.QObject):
|
|
||||||
|
|
||||||
def event(self, event):
|
|
||||||
event.fn(*event.args, **event.kwargs)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
_invoker = Invoker()
|
|
||||||
|
|
||||||
|
|
||||||
def invoke_in_main_thread(fn, *args, **kwargs):
|
|
||||||
QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs))
|
|
||||||
|
|
||||||
|
|
||||||
class FileTransfersThread(threading.Thread):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._queue = queue.Queue()
|
|
||||||
self._timeout = 0.01
|
|
||||||
self._continue = True
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def execute(self, function, *args, **kwargs):
|
|
||||||
self._queue.put((function, args, kwargs))
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._continue = False
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while self._continue:
|
|
||||||
try:
|
|
||||||
function, args, kwargs = self._queue.get(timeout=self._timeout)
|
|
||||||
function(*args, **kwargs)
|
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
except queue.Full:
|
|
||||||
util.log('Queue is Full in _thread')
|
|
||||||
except Exception as ex:
|
|
||||||
util.log('Exception in _thread: ' + str(ex))
|
|
||||||
|
|
||||||
|
|
||||||
_thread = FileTransfersThread()
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
_thread.start()
|
|
||||||
|
|
||||||
|
|
||||||
def stop():
|
|
||||||
_thread.stop()
|
|
||||||
_thread.join()
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Callbacks - current user
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def self_connection_status(tox_link):
|
|
||||||
"""
|
|
||||||
Current user changed connection status (offline, UDP, TCP)
|
|
||||||
"""
|
|
||||||
def wrapped(tox, connection, user_data):
|
|
||||||
print('Connection status: ', str(connection))
|
|
||||||
profile = Profile.get_instance()
|
|
||||||
if profile.status is None:
|
|
||||||
status = tox_link.self_get_status()
|
|
||||||
invoke_in_main_thread(profile.set_status, status)
|
|
||||||
elif connection == TOX_CONNECTION['NONE']:
|
|
||||||
invoke_in_main_thread(profile.set_status, None)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Callbacks - friends
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def friend_status(tox, friend_num, new_status, user_data):
|
|
||||||
"""
|
|
||||||
Check friend's status (none, busy, away)
|
|
||||||
"""
|
|
||||||
print("Friend's #{} status changed!".format(friend_num))
|
|
||||||
profile = Profile.get_instance()
|
|
||||||
friend = profile.get_friend_by_number(friend_num)
|
|
||||||
if friend.status is None and Settings.get_instance()['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']:
|
|
||||||
sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS'])
|
|
||||||
invoke_in_main_thread(friend.set_status, new_status)
|
|
||||||
invoke_in_main_thread(QtCore.QTimer.singleShot, 5000, lambda: profile.send_files(friend_num))
|
|
||||||
invoke_in_main_thread(profile.update_filtration)
|
|
||||||
|
|
||||||
|
|
||||||
def friend_connection_status(tox, friend_num, new_status, user_data):
|
|
||||||
"""
|
|
||||||
Check friend's connection status (offline, udp, tcp)
|
|
||||||
"""
|
|
||||||
print("Friend #{} connection status: {}".format(friend_num, new_status))
|
|
||||||
profile = Profile.get_instance()
|
|
||||||
friend = profile.get_friend_by_number(friend_num)
|
|
||||||
if new_status == TOX_CONNECTION['NONE']:
|
|
||||||
invoke_in_main_thread(profile.friend_exit, friend_num)
|
|
||||||
invoke_in_main_thread(profile.update_filtration)
|
|
||||||
if Settings.get_instance()['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']:
|
|
||||||
sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS'])
|
|
||||||
elif friend.status is None:
|
|
||||||
invoke_in_main_thread(profile.send_avatar, friend_num)
|
|
||||||
invoke_in_main_thread(PluginLoader.get_instance().friend_online, friend_num)
|
|
||||||
|
|
||||||
|
|
||||||
def friend_name(tox, friend_num, name, size, user_data):
|
|
||||||
"""
|
|
||||||
Friend changed his name
|
|
||||||
"""
|
|
||||||
profile = Profile.get_instance()
|
|
||||||
print('New name friend #' + str(friend_num))
|
|
||||||
invoke_in_main_thread(profile.new_name, friend_num, name)
|
|
||||||
|
|
||||||
|
|
||||||
def friend_status_message(tox, friend_num, status_message, size, user_data):
|
|
||||||
"""
|
|
||||||
:return: function for callback friend_status_message. It updates friend's status message
|
|
||||||
and calls window repaint
|
|
||||||
"""
|
|
||||||
profile = Profile.get_instance()
|
|
||||||
friend = profile.get_friend_by_number(friend_num)
|
|
||||||
invoke_in_main_thread(friend.set_status_message, status_message)
|
|
||||||
print('User #{} has new status'.format(friend_num))
|
|
||||||
invoke_in_main_thread(profile.send_messages, friend_num)
|
|
||||||
if profile.get_active_number() == friend_num:
|
|
||||||
invoke_in_main_thread(profile.set_active)
|
|
||||||
|
|
||||||
|
|
||||||
def friend_message(window, tray):
|
|
||||||
"""
|
|
||||||
New message from friend
|
|
||||||
"""
|
|
||||||
def wrapped(tox, friend_number, message_type, message, size, user_data):
|
|
||||||
profile = Profile.get_instance()
|
|
||||||
settings = Settings.get_instance()
|
|
||||||
message = str(message, 'utf-8')
|
|
||||||
invoke_in_main_thread(profile.new_message, friend_number, message_type, message)
|
|
||||||
if not window.isActiveWindow():
|
|
||||||
friend = profile.get_friend_by_number(friend_number)
|
|
||||||
if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked:
|
|
||||||
invoke_in_main_thread(tray_notification, friend.name, message, tray, window)
|
|
||||||
if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']:
|
|
||||||
sound_notification(SOUND_NOTIFICATION['MESSAGE'])
|
|
||||||
invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png'))
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def friend_request(tox, public_key, message, message_size, user_data):
|
|
||||||
"""
|
|
||||||
Called when user get new friend request
|
|
||||||
"""
|
|
||||||
print('Friend request')
|
|
||||||
profile = Profile.get_instance()
|
|
||||||
key = ''.join(chr(x) for x in public_key[:TOX_PUBLIC_KEY_SIZE])
|
|
||||||
tox_id = bin_to_string(key, TOX_PUBLIC_KEY_SIZE)
|
|
||||||
if tox_id not in Settings.get_instance()['blocked']:
|
|
||||||
invoke_in_main_thread(profile.process_friend_request, tox_id, str(message, 'utf-8'))
|
|
||||||
|
|
||||||
|
|
||||||
def friend_typing(tox, friend_number, typing, user_data):
|
|
||||||
invoke_in_main_thread(Profile.get_instance().friend_typing, friend_number, typing)
|
|
||||||
|
|
||||||
|
|
||||||
def friend_read_receipt(tox, friend_number, message_id, user_data):
|
|
||||||
profile = Profile.get_instance()
|
|
||||||
profile.get_friend_by_number(friend_number).dec_receipt()
|
|
||||||
if friend_number == profile.get_active_number():
|
|
||||||
invoke_in_main_thread(profile.receipt)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Callbacks - file transfers
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def tox_file_recv(window, tray):
|
|
||||||
"""
|
|
||||||
New incoming file
|
|
||||||
"""
|
|
||||||
def wrapped(tox, friend_number, file_number, file_type, size, file_name, file_name_size, user_data):
|
|
||||||
profile = Profile.get_instance()
|
|
||||||
settings = Settings.get_instance()
|
|
||||||
if file_type == TOX_FILE_KIND['DATA']:
|
|
||||||
print('File')
|
|
||||||
try:
|
|
||||||
file_name = str(file_name[:file_name_size], 'utf-8')
|
|
||||||
except:
|
|
||||||
file_name = 'toxygen_file'
|
|
||||||
invoke_in_main_thread(profile.incoming_file_transfer,
|
|
||||||
friend_number,
|
|
||||||
file_number,
|
|
||||||
size,
|
|
||||||
file_name)
|
|
||||||
if not window.isActiveWindow():
|
|
||||||
friend = profile.get_friend_by_number(friend_number)
|
|
||||||
if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked:
|
|
||||||
file_from = QtWidgets.QApplication.translate("Callback", "File from")
|
|
||||||
invoke_in_main_thread(tray_notification, file_from + ' ' + friend.name, file_name, tray, window)
|
|
||||||
if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']:
|
|
||||||
sound_notification(SOUND_NOTIFICATION['FILE_TRANSFER'])
|
|
||||||
invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png'))
|
|
||||||
else: # AVATAR
|
|
||||||
print('Avatar')
|
|
||||||
invoke_in_main_thread(profile.incoming_avatar,
|
|
||||||
friend_number,
|
|
||||||
file_number,
|
|
||||||
size)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def file_recv_chunk(tox, friend_number, file_number, position, chunk, length, user_data):
|
|
||||||
"""
|
|
||||||
Incoming chunk
|
|
||||||
"""
|
|
||||||
_thread.execute(Profile.get_instance().incoming_chunk, friend_number, file_number, position,
|
|
||||||
chunk[:length] if length else None)
|
|
||||||
|
|
||||||
|
|
||||||
def file_chunk_request(tox, friend_number, file_number, position, size, user_data):
|
|
||||||
"""
|
|
||||||
Outgoing chunk
|
|
||||||
"""
|
|
||||||
Profile.get_instance().outgoing_chunk(friend_number, file_number, position, size)
|
|
||||||
|
|
||||||
|
|
||||||
def file_recv_control(tox, friend_number, file_number, file_control, user_data):
|
|
||||||
"""
|
|
||||||
Friend cancelled, paused or resumed file transfer
|
|
||||||
"""
|
|
||||||
if file_control == TOX_FILE_CONTROL['CANCEL']:
|
|
||||||
invoke_in_main_thread(Profile.get_instance().cancel_transfer, friend_number, file_number, True)
|
|
||||||
elif file_control == TOX_FILE_CONTROL['PAUSE']:
|
|
||||||
invoke_in_main_thread(Profile.get_instance().pause_transfer, friend_number, file_number, True)
|
|
||||||
elif file_control == TOX_FILE_CONTROL['RESUME']:
|
|
||||||
invoke_in_main_thread(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
|
|
||||||
"""
|
|
||||||
data = data[:length]
|
|
||||||
plugin = PluginLoader.get_instance()
|
|
||||||
invoke_in_main_thread(plugin.callback_lossless, friend_number, data)
|
|
||||||
|
|
||||||
|
|
||||||
def lossy_packet(tox, friend_number, data, length, user_data):
|
|
||||||
"""
|
|
||||||
Incoming lossy packet
|
|
||||||
"""
|
|
||||||
data = data[:length]
|
|
||||||
plugin = PluginLoader.get_instance()
|
|
||||||
invoke_in_main_thread(plugin.callback_lossy, friend_number, data)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Callbacks - audio
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def call_state(toxav, friend_number, mask, user_data):
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
else:
|
|
||||||
Profile.get_instance().call.toxav_call_state_cb(friend_number, mask)
|
|
||||||
|
|
||||||
|
|
||||||
def call(toxav, friend_number, audio, video, user_data):
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
Profile.get_instance().call.audio_chunk(
|
|
||||||
bytes(samples[:audio_samples_per_channel * 2 * audio_channels_count]),
|
|
||||||
audio_channels_count,
|
|
||||||
rate)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Callbacks - video
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def video_receive_frame(toxav, friend_number, width, height, y, u, v, ystride, ustride, vstride, user_data):
|
|
||||||
"""
|
|
||||||
Creates yuv frame from y, u, v and shows it using OpenCV
|
|
||||||
For yuv => bgr we need this YUV420 frame:
|
|
||||||
|
|
||||||
width
|
|
||||||
-------------------------
|
|
||||||
| |
|
|
||||||
| Y | height
|
|
||||||
| |
|
|
||||||
-------------------------
|
|
||||||
| | |
|
|
||||||
| U even | U odd | height // 4
|
|
||||||
| | |
|
|
||||||
-------------------------
|
|
||||||
| | |
|
|
||||||
| V even | V odd | height // 4
|
|
||||||
| | |
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
width // 2 width // 2
|
|
||||||
|
|
||||||
It can be created from initial y, u, v using slices
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
y_size = abs(max(width, abs(ystride)))
|
|
||||||
u_size = abs(max(width // 2, abs(ustride)))
|
|
||||||
v_size = abs(max(width // 2, abs(vstride)))
|
|
||||||
|
|
||||||
y = np.asarray(y[:y_size * height], dtype=np.uint8).reshape(height, y_size)
|
|
||||||
u = np.asarray(u[:u_size * height // 2], dtype=np.uint8).reshape(height // 2, u_size)
|
|
||||||
v = np.asarray(v[:v_size * height // 2], dtype=np.uint8).reshape(height // 2, v_size)
|
|
||||||
|
|
||||||
width -= width % 4
|
|
||||||
height -= height % 4
|
|
||||||
|
|
||||||
frame = np.zeros((int(height * 1.5), width), dtype=np.uint8)
|
|
||||||
|
|
||||||
frame[:height, :] = y[:height, :width]
|
|
||||||
frame[height:height * 5 // 4, :width // 2] = u[:height // 2:2, :width // 2]
|
|
||||||
frame[height:height * 5 // 4, width // 2:] = u[1:height // 2:2, :width // 2]
|
|
||||||
|
|
||||||
frame[height * 5 // 4:, :width // 2] = v[:height // 2:2, :width // 2]
|
|
||||||
frame[height * 5 // 4:, width // 2:] = v[1:height // 2:2, :width // 2]
|
|
||||||
|
|
||||||
frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
|
|
||||||
|
|
||||||
invoke_in_main_thread(cv2.imshow, str(friend_number), frame)
|
|
||||||
except Exception as ex:
|
|
||||||
print(ex)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Callbacks - groups
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def group_invite(tox, friend_number, gc_type, data, length, user_data):
|
|
||||||
invoke_in_main_thread(Profile.get_instance().group_invite, friend_number, gc_type,
|
|
||||||
bytes(data[:length]))
|
|
||||||
|
|
||||||
|
|
||||||
def show_gc_notification(window, tray, message, group_number, peer_number):
|
|
||||||
profile = Profile.get_instance()
|
|
||||||
settings = Settings.get_instance()
|
|
||||||
chat = profile.get_group_by_number(group_number)
|
|
||||||
peer_name = chat.get_peer_name(peer_number)
|
|
||||||
if not window.isActiveWindow() and (profile.name in message or settings['group_notifications']):
|
|
||||||
if settings['notifications'] and profile.status != TOX_USER_STATUS['BUSY'] and not settings.locked:
|
|
||||||
invoke_in_main_thread(tray_notification, chat.name + ' ' + peer_name, message, tray, window)
|
|
||||||
if settings['sound_notifications'] and profile.status != TOX_USER_STATUS['BUSY']:
|
|
||||||
sound_notification(SOUND_NOTIFICATION['MESSAGE'])
|
|
||||||
invoke_in_main_thread(tray.setIcon, QtGui.QIcon(curr_directory() + '/images/icon_new_messages.png'))
|
|
||||||
|
|
||||||
|
|
||||||
def group_message(window, tray):
|
|
||||||
def wrapped(tox, group_number, peer_number, message, length, user_data):
|
|
||||||
message = str(message[:length], 'utf-8')
|
|
||||||
invoke_in_main_thread(Profile.get_instance().new_gc_message, group_number,
|
|
||||||
peer_number, TOX_MESSAGE_TYPE['NORMAL'], message)
|
|
||||||
show_gc_notification(window, tray, message, group_number, peer_number)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def group_action(window, tray):
|
|
||||||
def wrapped(tox, group_number, peer_number, message, length, user_data):
|
|
||||||
message = str(message[:length], 'utf-8')
|
|
||||||
invoke_in_main_thread(Profile.get_instance().new_gc_message, group_number,
|
|
||||||
peer_number, TOX_MESSAGE_TYPE['ACTION'], message)
|
|
||||||
show_gc_notification(window, tray, message, group_number, peer_number)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def group_title(tox, group_number, peer_number, title, length, user_data):
|
|
||||||
invoke_in_main_thread(Profile.get_instance().new_gc_title, group_number,
|
|
||||||
title[:length])
|
|
||||||
|
|
||||||
|
|
||||||
def group_namelist_change(tox, group_number, peer_number, change, user_data):
|
|
||||||
invoke_in_main_thread(Profile.get_instance().update_gc, group_number)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Callbacks - initialization
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def init_callbacks(tox, window, tray):
|
|
||||||
"""
|
|
||||||
Initialization of all callbacks.
|
|
||||||
:param tox: tox instance
|
|
||||||
:param window: main window
|
|
||||||
:param tray: tray (for notifications)
|
|
||||||
"""
|
|
||||||
tox.callback_self_connection_status(self_connection_status(tox), 0)
|
|
||||||
|
|
||||||
tox.callback_friend_status(friend_status, 0)
|
|
||||||
tox.callback_friend_message(friend_message(window, tray), 0)
|
|
||||||
tox.callback_friend_connection_status(friend_connection_status, 0)
|
|
||||||
tox.callback_friend_name(friend_name, 0)
|
|
||||||
tox.callback_friend_status_message(friend_status_message, 0)
|
|
||||||
tox.callback_friend_request(friend_request, 0)
|
|
||||||
tox.callback_friend_typing(friend_typing, 0)
|
|
||||||
tox.callback_friend_read_receipt(friend_read_receipt, 0)
|
|
||||||
|
|
||||||
tox.callback_file_recv(tox_file_recv(window, tray), 0)
|
|
||||||
tox.callback_file_recv_chunk(file_recv_chunk, 0)
|
|
||||||
tox.callback_file_chunk_request(file_chunk_request, 0)
|
|
||||||
tox.callback_file_recv_control(file_recv_control, 0)
|
|
||||||
|
|
||||||
toxav = tox.AV
|
|
||||||
toxav.callback_call_state(call_state, 0)
|
|
||||||
toxav.callback_call(call, 0)
|
|
||||||
toxav.callback_audio_receive_frame(callback_audio, 0)
|
|
||||||
toxav.callback_video_receive_frame(video_receive_frame, 0)
|
|
||||||
|
|
||||||
tox.callback_friend_lossless_packet(lossless_packet, 0)
|
|
||||||
tox.callback_friend_lossy_packet(lossy_packet, 0)
|
|
||||||
|
|
||||||
tox.callback_group_invite(group_invite)
|
|
||||||
tox.callback_group_message(group_message(window, tray))
|
|
||||||
tox.callback_group_action(group_action(window, tray))
|
|
||||||
tox.callback_group_title(group_title)
|
|
||||||
tox.callback_group_namelist_change(group_namelist_change)
|
|
339
toxygen/calls.py
339
toxygen/calls.py
|
@ -1,339 +0,0 @@
|
||||||
import pyaudio
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import settings
|
|
||||||
from toxav_enums import *
|
|
||||||
import cv2
|
|
||||||
import itertools
|
|
||||||
import numpy as np
|
|
||||||
import screen_sharing
|
|
||||||
# TODO: play sound until outgoing call will be started or cancelled
|
|
||||||
|
|
||||||
|
|
||||||
class Call:
|
|
||||||
|
|
||||||
def __init__(self, out_audio, out_video, in_audio=False, in_video=False):
|
|
||||||
self._in_audio = in_audio
|
|
||||||
self._in_video = in_video
|
|
||||||
self._out_audio = out_audio
|
|
||||||
self._out_video = out_video
|
|
||||||
self._is_active = False
|
|
||||||
|
|
||||||
def get_is_active(self):
|
|
||||||
return self._is_active
|
|
||||||
|
|
||||||
def set_is_active(self, value):
|
|
||||||
self._is_active = value
|
|
||||||
|
|
||||||
is_active = property(get_is_active, set_is_active)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Audio
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_in_audio(self):
|
|
||||||
return self._in_audio
|
|
||||||
|
|
||||||
def set_in_audio(self, value):
|
|
||||||
self._in_audio = value
|
|
||||||
|
|
||||||
in_audio = property(get_in_audio, set_in_audio)
|
|
||||||
|
|
||||||
def get_out_audio(self):
|
|
||||||
return self._out_audio
|
|
||||||
|
|
||||||
def set_out_audio(self, value):
|
|
||||||
self._out_audio = value
|
|
||||||
|
|
||||||
out_audio = property(get_out_audio, set_out_audio)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Video
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_in_video(self):
|
|
||||||
return self._in_video
|
|
||||||
|
|
||||||
def set_in_video(self, value):
|
|
||||||
self._in_video = value
|
|
||||||
|
|
||||||
in_video = property(get_in_video, set_in_video)
|
|
||||||
|
|
||||||
def get_out_video(self):
|
|
||||||
return self._out_video
|
|
||||||
|
|
||||||
def set_out_video(self, value):
|
|
||||||
self._out_video = value
|
|
||||||
|
|
||||||
out_video = property(get_out_video, set_out_video)
|
|
||||||
|
|
||||||
|
|
||||||
class AV:
|
|
||||||
|
|
||||||
def __init__(self, toxav):
|
|
||||||
self._toxav = toxav
|
|
||||||
self._running = True
|
|
||||||
|
|
||||||
self._calls = {} # dict: key - friend number, value - Call instance
|
|
||||||
|
|
||||||
self._audio = None
|
|
||||||
self._audio_stream = None
|
|
||||||
self._audio_thread = None
|
|
||||||
self._audio_running = False
|
|
||||||
self._out_stream = None
|
|
||||||
|
|
||||||
self._audio_rate = 8000
|
|
||||||
self._audio_channels = 1
|
|
||||||
self._audio_duration = 60
|
|
||||||
self._audio_sample_count = self._audio_rate * self._audio_channels * self._audio_duration // 1000
|
|
||||||
|
|
||||||
self._video = None
|
|
||||||
self._video_thread = None
|
|
||||||
self._video_running = False
|
|
||||||
|
|
||||||
self._video_width = 640
|
|
||||||
self._video_height = 480
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._running = False
|
|
||||||
self.stop_audio_thread()
|
|
||||||
self.stop_video_thread()
|
|
||||||
|
|
||||||
def __contains__(self, friend_number):
|
|
||||||
return friend_number in self._calls
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Calls
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def __call__(self, friend_number, audio, video):
|
|
||||||
"""Call friend with specified number"""
|
|
||||||
self._toxav.call(friend_number, 32 if audio else 0, 5000 if video else 0)
|
|
||||||
self._calls[friend_number] = Call(audio, video)
|
|
||||||
threading.Timer(30.0, lambda: self.finish_not_started_call(friend_number)).start()
|
|
||||||
|
|
||||||
def accept_call(self, friend_number, audio_enabled, video_enabled):
|
|
||||||
if self._running:
|
|
||||||
self._calls[friend_number] = Call(audio_enabled, video_enabled)
|
|
||||||
self._toxav.answer(friend_number, 32 if audio_enabled else 0, 5000 if video_enabled else 0)
|
|
||||||
if audio_enabled:
|
|
||||||
self.start_audio_thread()
|
|
||||||
if video_enabled:
|
|
||||||
self.start_video_thread()
|
|
||||||
|
|
||||||
def finish_call(self, friend_number, by_friend=False):
|
|
||||||
if not by_friend:
|
|
||||||
self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL'])
|
|
||||||
if friend_number in self._calls:
|
|
||||||
del self._calls[friend_number]
|
|
||||||
if not len(list(filter(lambda c: c.out_audio, self._calls))):
|
|
||||||
self.stop_audio_thread()
|
|
||||||
if not len(list(filter(lambda c: c.out_video, self._calls))):
|
|
||||||
self.stop_video_thread()
|
|
||||||
|
|
||||||
def finish_not_started_call(self, friend_number):
|
|
||||||
if friend_number in self:
|
|
||||||
call = self._calls[friend_number]
|
|
||||||
if not call.is_active:
|
|
||||||
self.finish_call(friend_number)
|
|
||||||
|
|
||||||
def toxav_call_state_cb(self, friend_number, state):
|
|
||||||
"""
|
|
||||||
New call state
|
|
||||||
"""
|
|
||||||
call = self._calls[friend_number]
|
|
||||||
call.is_active = True
|
|
||||||
|
|
||||||
call.in_audio = state | TOXAV_FRIEND_CALL_STATE['SENDING_A'] > 0
|
|
||||||
call.in_video = state | TOXAV_FRIEND_CALL_STATE['SENDING_V'] > 0
|
|
||||||
|
|
||||||
if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_A'] and call.out_audio:
|
|
||||||
self.start_audio_thread()
|
|
||||||
|
|
||||||
if state | TOXAV_FRIEND_CALL_STATE['ACCEPTING_V'] and call.out_video:
|
|
||||||
self.start_video_thread()
|
|
||||||
|
|
||||||
def is_video_call(self, number):
|
|
||||||
return number in self and self._calls[number].in_video
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Threads
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def start_audio_thread(self):
|
|
||||||
"""
|
|
||||||
Start audio sending
|
|
||||||
"""
|
|
||||||
if self._audio_thread is not None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._audio_running = True
|
|
||||||
|
|
||||||
self._audio = pyaudio.PyAudio()
|
|
||||||
self._audio_stream = self._audio.open(format=pyaudio.paInt16,
|
|
||||||
rate=self._audio_rate,
|
|
||||||
channels=self._audio_channels,
|
|
||||||
input=True,
|
|
||||||
input_device_index=settings.Settings.get_instance().audio['input'],
|
|
||||||
frames_per_buffer=self._audio_sample_count * 10)
|
|
||||||
|
|
||||||
self._audio_thread = threading.Thread(target=self.send_audio)
|
|
||||||
self._audio_thread.start()
|
|
||||||
|
|
||||||
def stop_audio_thread(self):
|
|
||||||
|
|
||||||
if self._audio_thread is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._audio_running = False
|
|
||||||
|
|
||||||
self._audio_thread.join()
|
|
||||||
|
|
||||||
self._audio_thread = None
|
|
||||||
self._audio_stream = None
|
|
||||||
self._audio = None
|
|
||||||
|
|
||||||
if self._out_stream is not None:
|
|
||||||
self._out_stream.stop_stream()
|
|
||||||
self._out_stream.close()
|
|
||||||
self._out_stream = None
|
|
||||||
|
|
||||||
def start_video_thread(self):
|
|
||||||
if self._video_thread is not None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._video_running = True
|
|
||||||
s = settings.Settings.get_instance()
|
|
||||||
self._video_width = s.video['width']
|
|
||||||
self._video_height = s.video['height']
|
|
||||||
|
|
||||||
if s.video['device'] == -1:
|
|
||||||
self._video = screen_sharing.DesktopGrabber(s.video['x'], s.video['y'],
|
|
||||||
s.video['width'], s.video['height'])
|
|
||||||
else:
|
|
||||||
self._video = cv2.VideoCapture(s.video['device'])
|
|
||||||
self._video.set(cv2.CAP_PROP_FPS, 25)
|
|
||||||
self._video.set(cv2.CAP_PROP_FRAME_WIDTH, self._video_width)
|
|
||||||
self._video.set(cv2.CAP_PROP_FRAME_HEIGHT, self._video_height)
|
|
||||||
|
|
||||||
self._video_thread = threading.Thread(target=self.send_video)
|
|
||||||
self._video_thread.start()
|
|
||||||
|
|
||||||
def stop_video_thread(self):
|
|
||||||
if self._video_thread is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._video_running = False
|
|
||||||
self._video_thread.join()
|
|
||||||
self._video_thread = None
|
|
||||||
self._video = None
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Incoming chunks
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def audio_chunk(self, samples, channels_count, rate):
|
|
||||||
"""
|
|
||||||
Incoming chunk
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._out_stream is None:
|
|
||||||
self._out_stream = self._audio.open(format=pyaudio.paInt16,
|
|
||||||
channels=channels_count,
|
|
||||||
rate=rate,
|
|
||||||
output_device_index=settings.Settings.get_instance().audio['output'],
|
|
||||||
output=True)
|
|
||||||
self._out_stream.write(samples)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# AV sending
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def send_audio(self):
|
|
||||||
"""
|
|
||||||
This method sends audio to friends
|
|
||||||
"""
|
|
||||||
|
|
||||||
while self._audio_running:
|
|
||||||
try:
|
|
||||||
pcm = self._audio_stream.read(self._audio_sample_count)
|
|
||||||
if pcm:
|
|
||||||
for friend_num in self._calls:
|
|
||||||
if self._calls[friend_num].out_audio:
|
|
||||||
try:
|
|
||||||
self._toxav.audio_send_frame(friend_num, pcm, self._audio_sample_count,
|
|
||||||
self._audio_channels, self._audio_rate)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
def send_video(self):
|
|
||||||
"""
|
|
||||||
This method sends video to friends
|
|
||||||
"""
|
|
||||||
while self._video_running:
|
|
||||||
try:
|
|
||||||
result, frame = self._video.read()
|
|
||||||
if result:
|
|
||||||
height, width, channels = frame.shape
|
|
||||||
for friend_num in self._calls:
|
|
||||||
if self._calls[friend_num].out_video:
|
|
||||||
try:
|
|
||||||
y, u, v = self.convert_bgr_to_yuv(frame)
|
|
||||||
self._toxav.video_send_frame(friend_num, width, height, y, u, v)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
def convert_bgr_to_yuv(self, frame):
|
|
||||||
"""
|
|
||||||
:param frame: input bgr frame
|
|
||||||
:return y, u, v: y, u, v values of frame
|
|
||||||
|
|
||||||
How this function works:
|
|
||||||
OpenCV creates YUV420 frame from BGR
|
|
||||||
This frame has following structure and size:
|
|
||||||
width, height - dim of input frame
|
|
||||||
width, height * 1.5 - dim of output frame
|
|
||||||
|
|
||||||
width
|
|
||||||
-------------------------
|
|
||||||
| |
|
|
||||||
| Y | height
|
|
||||||
| |
|
|
||||||
-------------------------
|
|
||||||
| | |
|
|
||||||
| U even | U odd | height // 4
|
|
||||||
| | |
|
|
||||||
-------------------------
|
|
||||||
| | |
|
|
||||||
| V even | V odd | height // 4
|
|
||||||
| | |
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
width // 2 width // 2
|
|
||||||
|
|
||||||
Y, U, V can be extracted using slices and joined in one list using itertools.chain.from_iterable()
|
|
||||||
Function returns bytes(y), bytes(u), bytes(v), because it is required for ctypes
|
|
||||||
"""
|
|
||||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420)
|
|
||||||
|
|
||||||
y = frame[:self._video_height, :]
|
|
||||||
y = list(itertools.chain.from_iterable(y))
|
|
||||||
|
|
||||||
u = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int)
|
|
||||||
u[::2, :] = frame[self._video_height:self._video_height * 5 // 4, :self._video_width // 2]
|
|
||||||
u[1::2, :] = frame[self._video_height:self._video_height * 5 // 4, self._video_width // 2:]
|
|
||||||
u = list(itertools.chain.from_iterable(u))
|
|
||||||
v = np.zeros((self._video_height // 2, self._video_width // 2), dtype=np.int)
|
|
||||||
v[::2, :] = frame[self._video_height * 5 // 4:, :self._video_width // 2]
|
|
||||||
v[1::2, :] = frame[self._video_height * 5 // 4:, self._video_width // 2:]
|
|
||||||
v = list(itertools.chain.from_iterable(v))
|
|
||||||
|
|
||||||
return bytes(y), bytes(u), bytes(v)
|
|
|
@ -1,288 +0,0 @@
|
||||||
from PyQt5 import QtCore, QtGui
|
|
||||||
from history import *
|
|
||||||
import basecontact
|
|
||||||
import util
|
|
||||||
from messages import *
|
|
||||||
import file_transfers as ft
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class Contact(basecontact.BaseContact):
|
|
||||||
"""
|
|
||||||
Class encapsulating TOX contact
|
|
||||||
Properties: number, message getter, history etc. Base class for friend and gc classes
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message_getter, number, name, status_message, widget, tox_id):
|
|
||||||
"""
|
|
||||||
:param message_getter: gets messages from db
|
|
||||||
:param number: number of friend.
|
|
||||||
"""
|
|
||||||
super().__init__(name, status_message, widget, tox_id)
|
|
||||||
self._number = number
|
|
||||||
self._new_messages = False
|
|
||||||
self._visible = True
|
|
||||||
self._alias = False
|
|
||||||
self._message_getter = message_getter
|
|
||||||
self._corr = []
|
|
||||||
self._unsaved_messages = 0
|
|
||||||
self._history_loaded = self._new_actions = False
|
|
||||||
self._curr_text = self._search_string = ''
|
|
||||||
self._search_index = 0
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self.set_visibility(False)
|
|
||||||
del self._widget
|
|
||||||
if hasattr(self, '_message_getter'):
|
|
||||||
del self._message_getter
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# History support
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def load_corr(self, first_time=True):
|
|
||||||
"""
|
|
||||||
:param first_time: friend became active, load first part of messages
|
|
||||||
"""
|
|
||||||
if (first_time and self._history_loaded) or (not hasattr(self, '_message_getter')):
|
|
||||||
return
|
|
||||||
if self._message_getter is None:
|
|
||||||
return
|
|
||||||
data = list(self._message_getter.get(PAGE_SIZE))
|
|
||||||
if data is not None and len(data):
|
|
||||||
data.reverse()
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
data = list(map(lambda tupl: TextMessage(*tupl), data))
|
|
||||||
self._corr = data + self._corr
|
|
||||||
self._history_loaded = True
|
|
||||||
|
|
||||||
def load_all_corr(self):
|
|
||||||
"""
|
|
||||||
Get all chat history from db for current friend
|
|
||||||
"""
|
|
||||||
if self._message_getter is None:
|
|
||||||
return
|
|
||||||
data = list(self._message_getter.get_all())
|
|
||||||
if data is not None and len(data):
|
|
||||||
data.reverse()
|
|
||||||
data = list(map(lambda tupl: TextMessage(*tupl), data))
|
|
||||||
self._corr = data + self._corr
|
|
||||||
self._history_loaded = True
|
|
||||||
|
|
||||||
def get_corr_for_saving(self):
|
|
||||||
"""
|
|
||||||
Get data to save in db
|
|
||||||
:return: list of unsaved messages or []
|
|
||||||
"""
|
|
||||||
messages = list(filter(lambda x: x.get_type() <= 1, self._corr))
|
|
||||||
return list(map(lambda x: x.get_data(), messages[-self._unsaved_messages:])) if self._unsaved_messages else []
|
|
||||||
|
|
||||||
def get_corr(self):
|
|
||||||
return self._corr[:]
|
|
||||||
|
|
||||||
def append_message(self, message):
|
|
||||||
"""
|
|
||||||
:param message: text or file transfer message
|
|
||||||
"""
|
|
||||||
self._corr.append(message)
|
|
||||||
if message.get_type() <= 1:
|
|
||||||
self._unsaved_messages += 1
|
|
||||||
|
|
||||||
def get_last_message_text(self):
|
|
||||||
messages = list(filter(lambda x: x.get_type() <= 1 and x.get_owner() != MESSAGE_OWNER['FRIEND'], self._corr))
|
|
||||||
if messages:
|
|
||||||
return messages[-1].get_data()[0]
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Unsent messages
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_unsent_messages(self):
|
|
||||||
"""
|
|
||||||
:return list of unsent messages
|
|
||||||
"""
|
|
||||||
messages = filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr)
|
|
||||||
return list(messages)
|
|
||||||
|
|
||||||
def get_unsent_messages_for_saving(self):
|
|
||||||
"""
|
|
||||||
:return list of unsent messages for saving
|
|
||||||
"""
|
|
||||||
messages = filter(lambda x: x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr)
|
|
||||||
return list(map(lambda x: x.get_data(), messages))
|
|
||||||
|
|
||||||
def mark_as_sent(self):
|
|
||||||
try:
|
|
||||||
message = list(filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr))[0]
|
|
||||||
message.mark_as_sent()
|
|
||||||
except Exception as ex:
|
|
||||||
util.log('Mark as sent ex: ' + str(ex))
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Message deletion
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def delete_message(self, time):
|
|
||||||
elem = list(filter(lambda x: type(x) in (TextMessage, GroupChatMessage) and x.get_data()[2] == time, self._corr))[0]
|
|
||||||
tmp = list(filter(lambda x: x.get_type() <= 1, self._corr))
|
|
||||||
if elem in tmp[-self._unsaved_messages:] and self._unsaved_messages:
|
|
||||||
self._unsaved_messages -= 1
|
|
||||||
self._corr.remove(elem)
|
|
||||||
self._message_getter.delete_one()
|
|
||||||
self._search_index = 0
|
|
||||||
|
|
||||||
def delete_old_messages(self):
|
|
||||||
"""
|
|
||||||
Delete old messages (reduces RAM usage if messages saving is not enabled)
|
|
||||||
"""
|
|
||||||
def save_message(x):
|
|
||||||
if x.get_type() == 2 and (x.get_status() >= 2 or x.get_status() is None):
|
|
||||||
return True
|
|
||||||
return x.get_owner() == MESSAGE_OWNER['NOT_SENT']
|
|
||||||
|
|
||||||
old = filter(save_message, self._corr[:-SAVE_MESSAGES])
|
|
||||||
self._corr = list(old) + self._corr[-SAVE_MESSAGES:]
|
|
||||||
text_messages = filter(lambda x: x.get_type() <= 1, self._corr)
|
|
||||||
self._unsaved_messages = min(self._unsaved_messages, len(list(text_messages)))
|
|
||||||
self._search_index = 0
|
|
||||||
|
|
||||||
def clear_corr(self, save_unsent=False):
|
|
||||||
"""
|
|
||||||
Clear messages list
|
|
||||||
"""
|
|
||||||
if hasattr(self, '_message_getter'):
|
|
||||||
del self._message_getter
|
|
||||||
self._search_index = 0
|
|
||||||
# don't delete data about active file transfer
|
|
||||||
if not save_unsent:
|
|
||||||
self._corr = list(filter(lambda x: x.get_type() == 2 and
|
|
||||||
x.get_status() in ft.ACTIVE_FILE_TRANSFERS, self._corr))
|
|
||||||
self._unsaved_messages = 0
|
|
||||||
else:
|
|
||||||
self._corr = list(filter(lambda x: (x.get_type() == 2 and x.get_status() in ft.ACTIVE_FILE_TRANSFERS)
|
|
||||||
or (x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT']),
|
|
||||||
self._corr))
|
|
||||||
self._unsaved_messages = len(self.get_unsent_messages())
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Chat history search
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def search_string(self, search_string):
|
|
||||||
self._search_string, self._search_index = search_string, 0
|
|
||||||
return self.search_prev()
|
|
||||||
|
|
||||||
def search_prev(self):
|
|
||||||
while True:
|
|
||||||
l = len(self._corr)
|
|
||||||
for i in range(self._search_index - 1, -l - 1, -1):
|
|
||||||
if self._corr[i].get_type() > 1:
|
|
||||||
continue
|
|
||||||
message = self._corr[i].get_data()[0]
|
|
||||||
if re.search(self._search_string, message, re.IGNORECASE) is not None:
|
|
||||||
self._search_index = i
|
|
||||||
return i
|
|
||||||
self._search_index = -l
|
|
||||||
self.load_corr(False)
|
|
||||||
if len(self._corr) == l:
|
|
||||||
return None # not found
|
|
||||||
|
|
||||||
def search_next(self):
|
|
||||||
if not self._search_index:
|
|
||||||
return None
|
|
||||||
for i in range(self._search_index + 1, 0):
|
|
||||||
if self._corr[i].get_type() > 1:
|
|
||||||
continue
|
|
||||||
message = self._corr[i].get_data()[0]
|
|
||||||
if re.search(self._search_string, message, re.IGNORECASE) is not None:
|
|
||||||
self._search_index = i
|
|
||||||
return i
|
|
||||||
return None # not found
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Current text - text from message area
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_curr_text(self):
|
|
||||||
return self._curr_text
|
|
||||||
|
|
||||||
def set_curr_text(self, value):
|
|
||||||
self._curr_text = value
|
|
||||||
|
|
||||||
curr_text = property(get_curr_text, set_curr_text)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Alias support
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def set_name(self, value):
|
|
||||||
"""
|
|
||||||
Set new name or ignore if alias exists
|
|
||||||
:param value: new name
|
|
||||||
"""
|
|
||||||
if not self._alias:
|
|
||||||
super().set_name(value)
|
|
||||||
|
|
||||||
def set_alias(self, alias):
|
|
||||||
self._alias = bool(alias)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Visibility in friends' list
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_visibility(self):
|
|
||||||
return self._visible
|
|
||||||
|
|
||||||
def set_visibility(self, value):
|
|
||||||
self._visible = value
|
|
||||||
|
|
||||||
visibility = property(get_visibility, set_visibility)
|
|
||||||
|
|
||||||
def set_widget(self, widget):
|
|
||||||
self._widget = widget
|
|
||||||
self.init_widget()
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Unread messages and other actions from friend
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_actions(self):
|
|
||||||
return self._new_actions
|
|
||||||
|
|
||||||
def set_actions(self, value):
|
|
||||||
self._new_actions = value
|
|
||||||
self._widget.connection_status.update(self.status, value)
|
|
||||||
|
|
||||||
actions = property(get_actions, set_actions) # unread messages, incoming files, av calls
|
|
||||||
|
|
||||||
def get_messages(self):
|
|
||||||
return self._new_messages
|
|
||||||
|
|
||||||
def inc_messages(self):
|
|
||||||
self._new_messages += 1
|
|
||||||
self._new_actions = True
|
|
||||||
self._widget.connection_status.update(self.status, True)
|
|
||||||
self._widget.messages.update(self._new_messages)
|
|
||||||
|
|
||||||
def reset_messages(self):
|
|
||||||
self._new_actions = False
|
|
||||||
self._new_messages = 0
|
|
||||||
self._widget.messages.update(self._new_messages)
|
|
||||||
self._widget.connection_status.update(self.status, False)
|
|
||||||
|
|
||||||
messages = property(get_messages)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Friend's number (can be used in toxcore)
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_number(self):
|
|
||||||
return self._number
|
|
||||||
|
|
||||||
def set_number(self, value):
|
|
||||||
self._number = value
|
|
||||||
|
|
||||||
number = property(get_number, set_number)
|
|
|
@ -1,347 +0,0 @@
|
||||||
from toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL
|
|
||||||
from os.path import basename, getsize, exists, dirname
|
|
||||||
from os import remove, rename, chdir
|
|
||||||
from time import time, sleep
|
|
||||||
from tox import Tox
|
|
||||||
import settings
|
|
||||||
from PyQt5 import QtCore
|
|
||||||
|
|
||||||
|
|
||||||
TOX_FILE_TRANSFER_STATE = {
|
|
||||||
'RUNNING': 0,
|
|
||||||
'PAUSED_BY_USER': 1,
|
|
||||||
'CANCELLED': 2,
|
|
||||||
'FINISHED': 3,
|
|
||||||
'PAUSED_BY_FRIEND': 4,
|
|
||||||
'INCOMING_NOT_STARTED': 5,
|
|
||||||
'OUTGOING_NOT_STARTED': 6
|
|
||||||
}
|
|
||||||
|
|
||||||
ACTIVE_FILE_TRANSFERS = (0, 1, 4, 5, 6)
|
|
||||||
|
|
||||||
PAUSED_FILE_TRANSFERS = (1, 4, 5, 6)
|
|
||||||
|
|
||||||
DO_NOT_SHOW_ACCEPT_BUTTON = (2, 3, 4, 6)
|
|
||||||
|
|
||||||
SHOW_PROGRESS_BAR = (0, 1, 4)
|
|
||||||
|
|
||||||
ALLOWED_FILES = ('toxygen_inline.png', 'utox-inline.png', 'sticker.png')
|
|
||||||
|
|
||||||
|
|
||||||
def is_inline(file_name):
|
|
||||||
return file_name in ALLOWED_FILES or file_name.startswith('qTox_Screenshot_') or file_name.startswith('qTox_Image_')
|
|
||||||
|
|
||||||
|
|
||||||
class StateSignal(QtCore.QObject):
|
|
||||||
|
|
||||||
signal = QtCore.pyqtSignal(int, float, int) # state, progress, time in sec
|
|
||||||
|
|
||||||
|
|
||||||
class TransferFinishedSignal(QtCore.QObject):
|
|
||||||
|
|
||||||
signal = QtCore.pyqtSignal(int, int) # friend number, file number
|
|
||||||
|
|
||||||
|
|
||||||
class FileTransfer(QtCore.QObject):
|
|
||||||
"""
|
|
||||||
Superclass for file transfers
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, path, tox, friend_number, size, file_number=None):
|
|
||||||
QtCore.QObject.__init__(self)
|
|
||||||
self._path = path
|
|
||||||
self._tox = tox
|
|
||||||
self._friend_number = friend_number
|
|
||||||
self.state = TOX_FILE_TRANSFER_STATE['RUNNING']
|
|
||||||
self._file_number = file_number
|
|
||||||
self._creation_time = None
|
|
||||||
self._size = float(size)
|
|
||||||
self._done = 0
|
|
||||||
self._state_changed = StateSignal()
|
|
||||||
self._finished = TransferFinishedSignal()
|
|
||||||
self._file_id = None
|
|
||||||
|
|
||||||
def set_tox(self, tox):
|
|
||||||
self._tox = tox
|
|
||||||
|
|
||||||
def set_state_changed_handler(self, handler):
|
|
||||||
self._state_changed.signal.connect(handler)
|
|
||||||
|
|
||||||
def set_transfer_finished_handler(self, handler):
|
|
||||||
self._finished.signal.connect(handler)
|
|
||||||
|
|
||||||
def signal(self):
|
|
||||||
percentage = self._done / self._size if self._size else 0
|
|
||||||
if self._creation_time is None or not percentage:
|
|
||||||
t = -1
|
|
||||||
else:
|
|
||||||
t = ((time() - self._creation_time) / percentage) * (1 - percentage)
|
|
||||||
self._state_changed.signal.emit(self.state, percentage, int(t))
|
|
||||||
|
|
||||||
def finished(self):
|
|
||||||
self._finished.signal.emit(self._friend_number, self._file_number)
|
|
||||||
|
|
||||||
def get_file_number(self):
|
|
||||||
return self._file_number
|
|
||||||
|
|
||||||
def get_friend_number(self):
|
|
||||||
return self._friend_number
|
|
||||||
|
|
||||||
def get_id(self):
|
|
||||||
return self._file_id
|
|
||||||
|
|
||||||
def get_path(self):
|
|
||||||
return self._path
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
self.send_control(TOX_FILE_CONTROL['CANCEL'])
|
|
||||||
if hasattr(self, '_file'):
|
|
||||||
self._file.close()
|
|
||||||
self.signal()
|
|
||||||
|
|
||||||
def cancelled(self):
|
|
||||||
if hasattr(self, '_file'):
|
|
||||||
sleep(0.1)
|
|
||||||
self._file.close()
|
|
||||||
self.state = TOX_FILE_TRANSFER_STATE['CANCELLED']
|
|
||||||
self.signal()
|
|
||||||
|
|
||||||
def pause(self, by_friend):
|
|
||||||
if not by_friend:
|
|
||||||
self.send_control(TOX_FILE_CONTROL['PAUSE'])
|
|
||||||
else:
|
|
||||||
self.state = TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND']
|
|
||||||
self.signal()
|
|
||||||
|
|
||||||
def send_control(self, control):
|
|
||||||
if self._tox.file_control(self._friend_number, self._file_number, control):
|
|
||||||
self.state = control
|
|
||||||
self.signal()
|
|
||||||
|
|
||||||
def get_file_id(self):
|
|
||||||
return self._tox.file_get_file_id(self._friend_number, self._file_number)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Send file
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class SendTransfer(FileTransfer):
|
|
||||||
|
|
||||||
def __init__(self, path, tox, friend_number, kind=TOX_FILE_KIND['DATA'], file_id=None):
|
|
||||||
if path is not None:
|
|
||||||
self._file = open(path, 'rb')
|
|
||||||
size = getsize(path)
|
|
||||||
else:
|
|
||||||
size = 0
|
|
||||||
super(SendTransfer, self).__init__(path, tox, friend_number, size)
|
|
||||||
self.state = TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']
|
|
||||||
self._file_number = tox.file_send(friend_number, kind, size, file_id,
|
|
||||||
bytes(basename(path), 'utf-8') if path else b'')
|
|
||||||
self._file_id = self.get_file_id()
|
|
||||||
|
|
||||||
def send_chunk(self, position, size):
|
|
||||||
"""
|
|
||||||
Send chunk
|
|
||||||
:param position: start position in file
|
|
||||||
:param size: chunk max size
|
|
||||||
"""
|
|
||||||
if self._creation_time is None:
|
|
||||||
self._creation_time = time()
|
|
||||||
if size:
|
|
||||||
self._file.seek(position)
|
|
||||||
data = self._file.read(size)
|
|
||||||
self._tox.file_send_chunk(self._friend_number, self._file_number, position, data)
|
|
||||||
self._done += size
|
|
||||||
else:
|
|
||||||
if hasattr(self, '_file'):
|
|
||||||
self._file.close()
|
|
||||||
self.state = TOX_FILE_TRANSFER_STATE['FINISHED']
|
|
||||||
self.finished()
|
|
||||||
self.signal()
|
|
||||||
|
|
||||||
|
|
||||||
class SendAvatar(SendTransfer):
|
|
||||||
"""
|
|
||||||
Send avatar to friend. Doesn't need file transfer item
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, path, tox, friend_number):
|
|
||||||
if path is None:
|
|
||||||
hash = None
|
|
||||||
else:
|
|
||||||
with open(path, 'rb') as fl:
|
|
||||||
hash = Tox.hash(fl.read())
|
|
||||||
super(SendAvatar, self).__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], hash)
|
|
||||||
|
|
||||||
|
|
||||||
class SendFromBuffer(FileTransfer):
|
|
||||||
"""
|
|
||||||
Send inline image
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, tox, friend_number, data, file_name):
|
|
||||||
super(SendFromBuffer, self).__init__(None, tox, friend_number, len(data))
|
|
||||||
self.state = TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']
|
|
||||||
self._data = data
|
|
||||||
self._file_number = tox.file_send(friend_number, TOX_FILE_KIND['DATA'],
|
|
||||||
len(data), None, bytes(file_name, 'utf-8'))
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return self._data
|
|
||||||
|
|
||||||
def send_chunk(self, position, size):
|
|
||||||
if self._creation_time is None:
|
|
||||||
self._creation_time = time()
|
|
||||||
if size:
|
|
||||||
data = self._data[position:position + size]
|
|
||||||
self._tox.file_send_chunk(self._friend_number, self._file_number, position, data)
|
|
||||||
self._done += size
|
|
||||||
else:
|
|
||||||
self.state = TOX_FILE_TRANSFER_STATE['FINISHED']
|
|
||||||
self.finished()
|
|
||||||
self.signal()
|
|
||||||
|
|
||||||
|
|
||||||
class SendFromFileBuffer(SendTransfer):
|
|
||||||
|
|
||||||
def __init__(self, *args):
|
|
||||||
super(SendFromFileBuffer, self).__init__(*args)
|
|
||||||
|
|
||||||
def send_chunk(self, position, size):
|
|
||||||
super(SendFromFileBuffer, self).send_chunk(position, size)
|
|
||||||
if not size:
|
|
||||||
chdir(dirname(self._path))
|
|
||||||
remove(self._path)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Receive file
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class ReceiveTransfer(FileTransfer):
|
|
||||||
|
|
||||||
def __init__(self, path, tox, friend_number, size, file_number, position=0):
|
|
||||||
super(ReceiveTransfer, self).__init__(path, tox, friend_number, size, file_number)
|
|
||||||
self._file = open(self._path, 'wb')
|
|
||||||
self._file_size = position
|
|
||||||
self._file.truncate(position)
|
|
||||||
self._missed = set()
|
|
||||||
self._file_id = self.get_file_id()
|
|
||||||
self._done = position
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
super(ReceiveTransfer, self).cancel()
|
|
||||||
remove(self._path)
|
|
||||||
|
|
||||||
def total_size(self):
|
|
||||||
self._missed.add(self._file_size)
|
|
||||||
return min(self._missed)
|
|
||||||
|
|
||||||
def write_chunk(self, position, data):
|
|
||||||
"""
|
|
||||||
Incoming chunk
|
|
||||||
:param position: position in file to save data
|
|
||||||
:param data: raw data (string)
|
|
||||||
"""
|
|
||||||
if self._creation_time is None:
|
|
||||||
self._creation_time = time()
|
|
||||||
if data is None:
|
|
||||||
self._file.close()
|
|
||||||
self.state = TOX_FILE_TRANSFER_STATE['FINISHED']
|
|
||||||
self.finished()
|
|
||||||
else:
|
|
||||||
data = bytearray(data)
|
|
||||||
if self._file_size < position:
|
|
||||||
self._file.seek(0, 2)
|
|
||||||
self._file.write(b'\0' * (position - self._file_size))
|
|
||||||
self._missed.add(self._file_size)
|
|
||||||
else:
|
|
||||||
self._missed.discard(position)
|
|
||||||
self._file.seek(position)
|
|
||||||
self._file.write(data)
|
|
||||||
l = len(data)
|
|
||||||
if position + l > self._file_size:
|
|
||||||
self._file_size = position + l
|
|
||||||
self._done += l
|
|
||||||
self.signal()
|
|
||||||
|
|
||||||
|
|
||||||
class ReceiveToBuffer(FileTransfer):
|
|
||||||
"""
|
|
||||||
Inline image - save in buffer not in file system
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, tox, friend_number, size, file_number):
|
|
||||||
super(ReceiveToBuffer, self).__init__(None, tox, friend_number, size, file_number)
|
|
||||||
self._data = bytes()
|
|
||||||
self._data_size = 0
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return self._data
|
|
||||||
|
|
||||||
def write_chunk(self, position, data):
|
|
||||||
if self._creation_time is None:
|
|
||||||
self._creation_time = time()
|
|
||||||
if data is None:
|
|
||||||
self.state = TOX_FILE_TRANSFER_STATE['FINISHED']
|
|
||||||
self.finished()
|
|
||||||
else:
|
|
||||||
data = bytes(data)
|
|
||||||
l = len(data)
|
|
||||||
if self._data_size < position:
|
|
||||||
self._data += (b'\0' * (position - self._data_size))
|
|
||||||
self._data = self._data[:position] + data + self._data[position + l:]
|
|
||||||
if position + l > self._data_size:
|
|
||||||
self._data_size = position + l
|
|
||||||
self._done += l
|
|
||||||
self.signal()
|
|
||||||
|
|
||||||
|
|
||||||
class ReceiveAvatar(ReceiveTransfer):
|
|
||||||
"""
|
|
||||||
Get friend's avatar. Doesn't need file transfer item
|
|
||||||
"""
|
|
||||||
MAX_AVATAR_SIZE = 512 * 1024
|
|
||||||
|
|
||||||
def __init__(self, tox, friend_number, size, file_number):
|
|
||||||
path = settings.ProfileHelper.get_path() + 'avatars/{}.png'.format(tox.friend_get_public_key(friend_number))
|
|
||||||
super(ReceiveAvatar, self).__init__(path + '.tmp', tox, friend_number, size, file_number)
|
|
||||||
if size > self.MAX_AVATAR_SIZE:
|
|
||||||
self.send_control(TOX_FILE_CONTROL['CANCEL'])
|
|
||||||
self._file.close()
|
|
||||||
remove(path + '.tmp')
|
|
||||||
elif not size:
|
|
||||||
self.send_control(TOX_FILE_CONTROL['CANCEL'])
|
|
||||||
self._file.close()
|
|
||||||
if exists(path):
|
|
||||||
remove(path)
|
|
||||||
self._file.close()
|
|
||||||
remove(path + '.tmp')
|
|
||||||
elif exists(path):
|
|
||||||
hash = self.get_file_id()
|
|
||||||
with open(path, 'rb') as fl:
|
|
||||||
data = fl.read()
|
|
||||||
existing_hash = Tox.hash(data)
|
|
||||||
if hash == existing_hash:
|
|
||||||
self.send_control(TOX_FILE_CONTROL['CANCEL'])
|
|
||||||
self._file.close()
|
|
||||||
remove(path + '.tmp')
|
|
||||||
else:
|
|
||||||
self.send_control(TOX_FILE_CONTROL['RESUME'])
|
|
||||||
else:
|
|
||||||
self.send_control(TOX_FILE_CONTROL['RESUME'])
|
|
||||||
|
|
||||||
def write_chunk(self, position, data):
|
|
||||||
super(ReceiveAvatar, self).write_chunk(position, data)
|
|
||||||
if self.state:
|
|
||||||
avatar_path = self._path[:-4]
|
|
||||||
if exists(avatar_path):
|
|
||||||
chdir(dirname(avatar_path))
|
|
||||||
remove(avatar_path)
|
|
||||||
rename(self._path, avatar_path)
|
|
||||||
self.finished(True)
|
|
||||||
|
|
||||||
def finished(self, emit=False):
|
|
||||||
if emit:
|
|
||||||
super().finished()
|
|
|
@ -1,75 +0,0 @@
|
||||||
import contact
|
|
||||||
from messages import *
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class Friend(contact.Contact):
|
|
||||||
"""
|
|
||||||
Friend in list of friends.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message_getter, number, name, status_message, widget, tox_id):
|
|
||||||
super().__init__(message_getter, number, name, status_message, widget, tox_id)
|
|
||||||
self._receipts = 0
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# File transfers support
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def update_transfer_data(self, file_number, status, inline=None):
|
|
||||||
"""
|
|
||||||
Update status of active transfer and load inline if needed
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
tr = list(filter(lambda x: x.get_type() == MESSAGE_TYPE['FILE_TRANSFER'] and x.is_active(file_number),
|
|
||||||
self._corr))[0]
|
|
||||||
tr.set_status(status)
|
|
||||||
i = self._corr.index(tr)
|
|
||||||
if inline: # inline was loaded
|
|
||||||
self._corr.insert(i, inline)
|
|
||||||
return i - len(self._corr)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_unsent_files(self):
|
|
||||||
messages = filter(lambda x: type(x) is UnsentFile, self._corr)
|
|
||||||
return messages
|
|
||||||
|
|
||||||
def clear_unsent_files(self):
|
|
||||||
self._corr = list(filter(lambda x: type(x) is not UnsentFile, self._corr))
|
|
||||||
|
|
||||||
def remove_invalid_unsent_files(self):
|
|
||||||
def is_valid(message):
|
|
||||||
if type(message) is not UnsentFile:
|
|
||||||
return True
|
|
||||||
if message.get_data()[1] is not None:
|
|
||||||
return True
|
|
||||||
return os.path.exists(message.get_data()[0])
|
|
||||||
self._corr = list(filter(is_valid, self._corr))
|
|
||||||
|
|
||||||
def delete_one_unsent_file(self, time):
|
|
||||||
self._corr = list(filter(lambda x: not (type(x) is UnsentFile and x.get_data()[2] == time), self._corr))
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# History support
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_receipts(self):
|
|
||||||
return self._receipts
|
|
||||||
|
|
||||||
receipts = property(get_receipts) # read receipts
|
|
||||||
|
|
||||||
def inc_receipts(self):
|
|
||||||
self._receipts += 1
|
|
||||||
|
|
||||||
def dec_receipt(self):
|
|
||||||
if self._receipts:
|
|
||||||
self._receipts -= 1
|
|
||||||
self.mark_as_sent()
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Full status
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def get_full_status(self):
|
|
||||||
return self._status_message
|
|
|
@ -1,49 +0,0 @@
|
||||||
import contact
|
|
||||||
import util
|
|
||||||
from PyQt5 import QtGui, QtCore
|
|
||||||
import toxcore_enums_and_consts as constants
|
|
||||||
|
|
||||||
|
|
||||||
class GroupChat(contact.Contact):
|
|
||||||
|
|
||||||
def __init__(self, name, status_message, widget, tox, group_number):
|
|
||||||
super().__init__(None, group_number, name, status_message, widget, None)
|
|
||||||
self._tox = tox
|
|
||||||
self.set_status(constants.TOX_USER_STATUS['NONE'])
|
|
||||||
|
|
||||||
def set_name(self, name):
|
|
||||||
self._tox.group_set_title(self._number, name)
|
|
||||||
super().set_name(name)
|
|
||||||
|
|
||||||
def send_message(self, message):
|
|
||||||
self._tox.group_message_send(self._number, message.encode('utf-8'))
|
|
||||||
|
|
||||||
def new_title(self, title):
|
|
||||||
super().set_name(title)
|
|
||||||
|
|
||||||
def load_avatar(self):
|
|
||||||
path = util.curr_directory() + '/images/group.png'
|
|
||||||
width = self._widget.avatar_label.width()
|
|
||||||
pixmap = QtGui.QPixmap(path)
|
|
||||||
self._widget.avatar_label.setPixmap(pixmap.scaled(width, width, QtCore.Qt.KeepAspectRatio,
|
|
||||||
QtCore.Qt.SmoothTransformation))
|
|
||||||
self._widget.avatar_label.repaint()
|
|
||||||
|
|
||||||
def remove_invalid_unsent_files(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_names(self):
|
|
||||||
peers_count = self._tox.group_number_peers(self._number)
|
|
||||||
names = []
|
|
||||||
for i in range(peers_count):
|
|
||||||
name = self._tox.group_peername(self._number, i)
|
|
||||||
names.append(name)
|
|
||||||
names = sorted(names, key=lambda n: n.lower())
|
|
||||||
return names
|
|
||||||
|
|
||||||
def get_full_status(self):
|
|
||||||
names = self.get_names()
|
|
||||||
return '\n'.join(names)
|
|
||||||
|
|
||||||
def get_peer_name(self, peer_number):
|
|
||||||
return self._tox.group_peername(self._number, peer_number)
|
|
|
@ -1,215 +0,0 @@
|
||||||
from sqlite3 import connect
|
|
||||||
import settings
|
|
||||||
from os import chdir
|
|
||||||
import os.path
|
|
||||||
from toxes import ToxES
|
|
||||||
|
|
||||||
|
|
||||||
PAGE_SIZE = 42
|
|
||||||
|
|
||||||
TIMEOUT = 11
|
|
||||||
|
|
||||||
SAVE_MESSAGES = 250
|
|
||||||
|
|
||||||
MESSAGE_OWNER = {
|
|
||||||
'ME': 0,
|
|
||||||
'FRIEND': 1,
|
|
||||||
'NOT_SENT': 2
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class History:
|
|
||||||
|
|
||||||
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 = ToxES.get_instance()
|
|
||||||
try:
|
|
||||||
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)
|
|
||||||
except:
|
|
||||||
os.remove(path)
|
|
||||||
db = connect(name + '.hstr', timeout=TIMEOUT)
|
|
||||||
cursor = db.cursor()
|
|
||||||
cursor.execute('CREATE TABLE IF NOT EXISTS friends('
|
|
||||||
' tox_id TEXT PRIMARY KEY'
|
|
||||||
')')
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
encr = ToxES.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(bytes(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'
|
|
||||||
with open(path, 'rb') as fin:
|
|
||||||
data = fin.read()
|
|
||||||
encr = ToxES.get_instance()
|
|
||||||
if encr.has_password():
|
|
||||||
data = encr.pass_encrypt(data)
|
|
||||||
with open(new_path, 'wb') as fout:
|
|
||||||
fout.write(data)
|
|
||||||
|
|
||||||
def add_friend_to_db(self, tox_id):
|
|
||||||
chdir(settings.ProfileHelper.get_path())
|
|
||||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
|
||||||
try:
|
|
||||||
cursor = db.cursor()
|
|
||||||
cursor.execute('INSERT INTO friends VALUES (?);', (tox_id, ))
|
|
||||||
cursor.execute('CREATE TABLE id' + tox_id + '('
|
|
||||||
' id INTEGER PRIMARY KEY,'
|
|
||||||
' message TEXT,'
|
|
||||||
' owner INTEGER,'
|
|
||||||
' unix_time REAL,'
|
|
||||||
' message_type INTEGER'
|
|
||||||
')')
|
|
||||||
db.commit()
|
|
||||||
except:
|
|
||||||
print('Database is locked!')
|
|
||||||
db.rollback()
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def delete_friend_from_db(self, tox_id):
|
|
||||||
chdir(settings.ProfileHelper.get_path())
|
|
||||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
|
||||||
try:
|
|
||||||
cursor = db.cursor()
|
|
||||||
cursor.execute('DELETE FROM friends WHERE tox_id=?;', (tox_id, ))
|
|
||||||
cursor.execute('DROP TABLE id' + tox_id + ';')
|
|
||||||
db.commit()
|
|
||||||
except:
|
|
||||||
print('Database is locked!')
|
|
||||||
db.rollback()
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def friend_exists_in_db(self, tox_id):
|
|
||||||
chdir(settings.ProfileHelper.get_path())
|
|
||||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
|
||||||
cursor = db.cursor()
|
|
||||||
cursor.execute('SELECT 0 FROM friends WHERE tox_id=?', (tox_id, ))
|
|
||||||
result = cursor.fetchone()
|
|
||||||
db.close()
|
|
||||||
return result is not None
|
|
||||||
|
|
||||||
def save_messages_to_db(self, tox_id, messages_iter):
|
|
||||||
chdir(settings.ProfileHelper.get_path())
|
|
||||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
|
||||||
try:
|
|
||||||
cursor = db.cursor()
|
|
||||||
cursor.executemany('INSERT INTO id' + tox_id + '(message, owner, unix_time, message_type) '
|
|
||||||
'VALUES (?, ?, ?, ?);', messages_iter)
|
|
||||||
db.commit()
|
|
||||||
except:
|
|
||||||
print('Database is locked!')
|
|
||||||
db.rollback()
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def update_messages(self, tox_id, unsent_time):
|
|
||||||
chdir(settings.ProfileHelper.get_path())
|
|
||||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
|
||||||
try:
|
|
||||||
cursor = db.cursor()
|
|
||||||
cursor.execute('UPDATE id' + tox_id + ' SET owner = 0 '
|
|
||||||
'WHERE unix_time < ' + str(unsent_time) + ' AND owner = 2;')
|
|
||||||
db.commit()
|
|
||||||
except:
|
|
||||||
print('Database is locked!')
|
|
||||||
db.rollback()
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def delete_message(self, tox_id, time):
|
|
||||||
start, end = str(time - 0.01), str(time + 0.01)
|
|
||||||
chdir(settings.ProfileHelper.get_path())
|
|
||||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
|
||||||
try:
|
|
||||||
cursor = db.cursor()
|
|
||||||
cursor.execute('DELETE FROM id' + tox_id + ' WHERE unix_time < ' + end + ' AND unix_time > ' +
|
|
||||||
start + ';')
|
|
||||||
db.commit()
|
|
||||||
except:
|
|
||||||
print('Database is locked!')
|
|
||||||
db.rollback()
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def delete_messages(self, tox_id):
|
|
||||||
chdir(settings.ProfileHelper.get_path())
|
|
||||||
db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
|
||||||
try:
|
|
||||||
cursor = db.cursor()
|
|
||||||
cursor.execute('DELETE FROM id' + tox_id + ';')
|
|
||||||
db.commit()
|
|
||||||
except:
|
|
||||||
print('Database is locked!')
|
|
||||||
db.rollback()
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def messages_getter(self, tox_id):
|
|
||||||
return History.MessageGetter(self._name, tox_id)
|
|
||||||
|
|
||||||
class MessageGetter:
|
|
||||||
|
|
||||||
def __init__(self, name, tox_id):
|
|
||||||
self._count = 0
|
|
||||||
self._name = name
|
|
||||||
self._tox_id = tox_id
|
|
||||||
self._db = self._cursor = None
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
chdir(settings.ProfileHelper.get_path())
|
|
||||||
self._db = connect(self._name + '.hstr', timeout=TIMEOUT)
|
|
||||||
self._cursor = self._db.cursor()
|
|
||||||
self._cursor.execute('SELECT message, owner, unix_time, message_type FROM id' + self._tox_id +
|
|
||||||
' ORDER BY unix_time DESC;')
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
self._db.close()
|
|
||||||
|
|
||||||
def get_one(self):
|
|
||||||
self.connect()
|
|
||||||
self.skip()
|
|
||||||
data = self._cursor.fetchone()
|
|
||||||
self._count += 1
|
|
||||||
self.disconnect()
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_all(self):
|
|
||||||
self.connect()
|
|
||||||
data = self._cursor.fetchall()
|
|
||||||
self.disconnect()
|
|
||||||
self._count = len(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get(self, count):
|
|
||||||
self.connect()
|
|
||||||
self.skip()
|
|
||||||
data = self._cursor.fetchmany(count)
|
|
||||||
self.disconnect()
|
|
||||||
self._count += len(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def skip(self):
|
|
||||||
if self._count:
|
|
||||||
self._cursor.fetchmany(self._count)
|
|
||||||
|
|
||||||
def delete_one(self):
|
|
||||||
if self._count:
|
|
||||||
self._count -= 1
|
|
|
@ -1,68 +0,0 @@
|
||||||
from PyQt5 import QtWidgets, QtCore
|
|
||||||
from list_items import *
|
|
||||||
|
|
||||||
|
|
||||||
class ItemsFactory:
|
|
||||||
|
|
||||||
def __init__(self, friends_list, messages):
|
|
||||||
self._friends = friends_list
|
|
||||||
self._messages = messages
|
|
||||||
|
|
||||||
def friend_item(self):
|
|
||||||
item = ContactItem()
|
|
||||||
elem = QtWidgets.QListWidgetItem(self._friends)
|
|
||||||
elem.setSizeHint(QtCore.QSize(250, item.height()))
|
|
||||||
self._friends.addItem(elem)
|
|
||||||
self._friends.setItemWidget(elem, item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
def message_item(self, text, time, name, sent, message_type, append, pixmap):
|
|
||||||
item = MessageItem(text, time, name, sent, message_type, self._messages)
|
|
||||||
if pixmap is not None:
|
|
||||||
item.set_avatar(pixmap)
|
|
||||||
elem = QtWidgets.QListWidgetItem()
|
|
||||||
elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height()))
|
|
||||||
if append:
|
|
||||||
self._messages.addItem(elem)
|
|
||||||
else:
|
|
||||||
self._messages.insertItem(0, elem)
|
|
||||||
self._messages.setItemWidget(elem, item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
def inline_item(self, data, append):
|
|
||||||
elem = QtWidgets.QListWidgetItem()
|
|
||||||
item = InlineImageItem(data, self._messages.width(), elem)
|
|
||||||
elem.setSizeHint(QtCore.QSize(self._messages.width(), item.height()))
|
|
||||||
if append:
|
|
||||||
self._messages.addItem(elem)
|
|
||||||
else:
|
|
||||||
self._messages.insertItem(0, elem)
|
|
||||||
self._messages.setItemWidget(elem, item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
def unsent_file_item(self, file_name, size, name, time, append):
|
|
||||||
item = UnsentFileItem(file_name,
|
|
||||||
size,
|
|
||||||
name,
|
|
||||||
time,
|
|
||||||
self._messages.width())
|
|
||||||
elem = QtWidgets.QListWidgetItem()
|
|
||||||
elem.setSizeHint(QtCore.QSize(self._messages.width() - 30, 34))
|
|
||||||
if append:
|
|
||||||
self._messages.addItem(elem)
|
|
||||||
else:
|
|
||||||
self._messages.insertItem(0, elem)
|
|
||||||
self._messages.setItemWidget(elem, item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
def file_transfer_item(self, data, append):
|
|
||||||
data.append(self._messages.width())
|
|
||||||
item = FileTransferItem(*data)
|
|
||||||
elem = QtWidgets.QListWidgetItem()
|
|
||||||
elem.setSizeHint(QtCore.QSize(self._messages.width() - 30, 34))
|
|
||||||
if append:
|
|
||||||
self._messages.addItem(elem)
|
|
||||||
else:
|
|
||||||
self._messages.insertItem(0, elem)
|
|
||||||
self._messages.setItemWidget(elem, item)
|
|
||||||
return item
|
|
|
@ -1,59 +0,0 @@
|
||||||
from platform import system
|
|
||||||
from ctypes import CDLL
|
|
||||||
import util
|
|
||||||
|
|
||||||
|
|
||||||
class LibToxCore:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if system() == 'Windows':
|
|
||||||
self._libtoxcore = CDLL(util.curr_directory() + '/libs/libtox.dll')
|
|
||||||
elif system() == 'Darwin':
|
|
||||||
self._libtoxcore = CDLL('libtoxcore.dylib')
|
|
||||||
else:
|
|
||||||
# libtoxcore and libsodium must be installed in your os
|
|
||||||
try:
|
|
||||||
self._libtoxcore = CDLL('libtoxcore.so')
|
|
||||||
except:
|
|
||||||
self._libtoxcore = CDLL(util.curr_directory() + '/libs/libtoxcore.so')
|
|
||||||
|
|
||||||
def __getattr__(self, item):
|
|
||||||
return self._libtoxcore.__getattr__(item)
|
|
||||||
|
|
||||||
|
|
||||||
class LibToxAV:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if system() == 'Windows':
|
|
||||||
# on Windows av api is in libtox.dll
|
|
||||||
self._libtoxav = CDLL(util.curr_directory() + '/libs/libtox.dll')
|
|
||||||
elif system() == 'Darwin':
|
|
||||||
self._libtoxav = CDLL('libtoxav.dylib')
|
|
||||||
else:
|
|
||||||
# /usr/lib/libtoxav.so must exists
|
|
||||||
try:
|
|
||||||
self._libtoxav = CDLL('libtoxav.so')
|
|
||||||
except:
|
|
||||||
self._libtoxav = CDLL(util.curr_directory() + '/libs/libtoxav.so')
|
|
||||||
|
|
||||||
def __getattr__(self, item):
|
|
||||||
return self._libtoxav.__getattr__(item)
|
|
||||||
|
|
||||||
|
|
||||||
class LibToxEncryptSave:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if system() == 'Windows':
|
|
||||||
# on Windows profile encryption api is in libtox.dll
|
|
||||||
self._lib_tox_encrypt_save = CDLL(util.curr_directory() + '/libs/libtox.dll')
|
|
||||||
elif system() == 'Darwin':
|
|
||||||
self._lib_tox_encrypt_save = CDLL('libtoxencryptsave.dylib')
|
|
||||||
else:
|
|
||||||
# /usr/lib/libtoxencryptsave.so must exists
|
|
||||||
try:
|
|
||||||
self._lib_tox_encrypt_save = CDLL('libtoxencryptsave.so')
|
|
||||||
except:
|
|
||||||
self._lib_tox_encrypt_save = CDLL(util.curr_directory() + '/libs/libtoxencryptsave.so')
|
|
||||||
|
|
||||||
def __getattr__(self, item):
|
|
||||||
return self._lib_tox_encrypt_save.__getattr__(item)
|
|
|
@ -1,545 +0,0 @@
|
||||||
from toxcore_enums_and_consts import *
|
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
||||||
import profile
|
|
||||||
from file_transfers import TOX_FILE_TRANSFER_STATE, PAUSED_FILE_TRANSFERS, DO_NOT_SHOW_ACCEPT_BUTTON, ACTIVE_FILE_TRANSFERS, SHOW_PROGRESS_BAR
|
|
||||||
from util import curr_directory, convert_time, curr_time
|
|
||||||
from widgets import DataLabel, create_menu
|
|
||||||
import html as h
|
|
||||||
import smileys
|
|
||||||
import settings
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class MessageEdit(QtWidgets.QTextBrowser):
|
|
||||||
|
|
||||||
def __init__(self, text, width, message_type, parent=None):
|
|
||||||
super(MessageEdit, self).__init__(parent)
|
|
||||||
self.urls = {}
|
|
||||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
||||||
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
||||||
self.setWordWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere)
|
|
||||||
self.document().setTextWidth(width)
|
|
||||||
self.setOpenExternalLinks(True)
|
|
||||||
self.setAcceptRichText(True)
|
|
||||||
self.setOpenLinks(False)
|
|
||||||
path = smileys.SmileyLoader.get_instance().get_smileys_path()
|
|
||||||
if path is not None:
|
|
||||||
self.setSearchPaths([path])
|
|
||||||
self.document().setDefaultStyleSheet('a { color: #306EFF; }')
|
|
||||||
text = self.decoratedText(text)
|
|
||||||
if message_type != TOX_MESSAGE_TYPE['NORMAL']:
|
|
||||||
self.setHtml('<p style="color: #5CB3FF; font: italic; font-size: 20px;" >' + text + '</p>')
|
|
||||||
else:
|
|
||||||
self.setHtml(text)
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setFamily(settings.Settings.get_instance()['font'])
|
|
||||||
font.setPixelSize(settings.Settings.get_instance()['message_font_size'])
|
|
||||||
font.setBold(False)
|
|
||||||
self.setFont(font)
|
|
||||||
self.resize(width, self.document().size().height())
|
|
||||||
self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse)
|
|
||||||
self.anchorClicked.connect(self.on_anchor_clicked)
|
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
|
||||||
menu = create_menu(self.createStandardContextMenu(event.pos()))
|
|
||||||
quote = menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Quote selected text'))
|
|
||||||
quote.triggered.connect(self.quote_text)
|
|
||||||
text = self.textCursor().selection().toPlainText()
|
|
||||||
if not text:
|
|
||||||
quote.setEnabled(False)
|
|
||||||
else:
|
|
||||||
import plugin_support
|
|
||||||
submenu = plugin_support.PluginLoader.get_instance().get_message_menu(menu, text)
|
|
||||||
if len(submenu):
|
|
||||||
plug = menu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Plugins'))
|
|
||||||
plug.addActions(submenu)
|
|
||||||
menu.popup(event.globalPos())
|
|
||||||
menu.exec_(event.globalPos())
|
|
||||||
del menu
|
|
||||||
|
|
||||||
def quote_text(self):
|
|
||||||
text = self.textCursor().selection().toPlainText()
|
|
||||||
if text:
|
|
||||||
import mainscreen
|
|
||||||
window = mainscreen.MainWindow.get_instance()
|
|
||||||
text = '>' + '\n>'.join(text.split('\n'))
|
|
||||||
if window.messageEdit.toPlainText():
|
|
||||||
text = '\n' + text
|
|
||||||
window.messageEdit.appendPlainText(text)
|
|
||||||
|
|
||||||
def on_anchor_clicked(self, url):
|
|
||||||
text = str(url.toString())
|
|
||||||
if text.startswith('tox:'):
|
|
||||||
import menu
|
|
||||||
self.add_contact = menu.AddContact(text[4:])
|
|
||||||
self.add_contact.show()
|
|
||||||
else:
|
|
||||||
QtGui.QDesktopServices.openUrl(url)
|
|
||||||
self.clearFocus()
|
|
||||||
|
|
||||||
def addAnimation(self, url, fileName):
|
|
||||||
movie = QtGui.QMovie(self)
|
|
||||||
movie.setFileName(fileName)
|
|
||||||
self.urls[movie] = url
|
|
||||||
movie.frameChanged[int].connect(lambda x: self.animate(movie))
|
|
||||||
movie.start()
|
|
||||||
|
|
||||||
def animate(self, movie):
|
|
||||||
self.document().addResource(QtGui.QTextDocument.ImageResource,
|
|
||||||
self.urls[movie],
|
|
||||||
movie.currentPixmap())
|
|
||||||
self.setLineWrapColumnOrWidth(self.lineWrapColumnOrWidth())
|
|
||||||
|
|
||||||
def decoratedText(self, text):
|
|
||||||
text = h.escape(text) # replace < and >
|
|
||||||
exp = QtCore.QRegExp(
|
|
||||||
'('
|
|
||||||
'(?:\\b)((www\\.)|(http[s]?|ftp)://)'
|
|
||||||
'\\w+\\S+)'
|
|
||||||
'|(?:\\b)(file:///)([\\S| ]*)'
|
|
||||||
'|(?:\\b)(tox:[a-zA-Z\\d]{76}$)'
|
|
||||||
'|(?:\\b)(mailto:\\S+@\\S+\\.\\S+)'
|
|
||||||
'|(?:\\b)(tox:\\S+@\\S+)')
|
|
||||||
offset = exp.indexIn(text, 0)
|
|
||||||
while offset != -1: # add links
|
|
||||||
url = exp.cap()
|
|
||||||
if exp.cap(2) == 'www.':
|
|
||||||
html = '<a href="http://{0}">{0}</a>'.format(url)
|
|
||||||
else:
|
|
||||||
html = '<a href="{0}">{0}</a>'.format(url)
|
|
||||||
text = text[:offset] + html + text[offset + len(exp.cap()):]
|
|
||||||
offset += len(html)
|
|
||||||
offset = exp.indexIn(text, offset)
|
|
||||||
arr = text.split('\n')
|
|
||||||
for i in range(len(arr)): # quotes
|
|
||||||
if arr[i].startswith('>'):
|
|
||||||
arr[i] = '<font color="green"><b>' + arr[i][4:] + '</b></font>'
|
|
||||||
text = '<br>'.join(arr)
|
|
||||||
text = smileys.SmileyLoader.get_instance().add_smileys_to_text(text, self) # smileys
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
class MessageItem(QtWidgets.QWidget):
|
|
||||||
"""
|
|
||||||
Message in messages list
|
|
||||||
"""
|
|
||||||
def __init__(self, text, time, user='', sent=True, message_type=TOX_MESSAGE_TYPE['NORMAL'], parent=None):
|
|
||||||
QtWidgets.QWidget.__init__(self, parent)
|
|
||||||
self.name = DataLabel(self)
|
|
||||||
self.name.setGeometry(QtCore.QRect(2, 2, 95, 23))
|
|
||||||
self.name.setTextFormat(QtCore.Qt.PlainText)
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setFamily(settings.Settings.get_instance()['font'])
|
|
||||||
font.setPointSize(11)
|
|
||||||
font.setBold(True)
|
|
||||||
self.name.setFont(font)
|
|
||||||
self.name.setText(user)
|
|
||||||
|
|
||||||
self.time = QtWidgets.QLabel(self)
|
|
||||||
self.time.setGeometry(QtCore.QRect(parent.width() - 60, 0, 50, 25))
|
|
||||||
font.setPointSize(10)
|
|
||||||
font.setBold(False)
|
|
||||||
self.time.setFont(font)
|
|
||||||
self._time = time
|
|
||||||
if not sent:
|
|
||||||
movie = QtGui.QMovie(curr_directory() + '/images/spinner.gif')
|
|
||||||
self.time.setMovie(movie)
|
|
||||||
movie.start()
|
|
||||||
self.t = True
|
|
||||||
else:
|
|
||||||
self.time.setText(convert_time(time))
|
|
||||||
self.t = False
|
|
||||||
|
|
||||||
self.message = MessageEdit(text, parent.width() - 160, message_type, self)
|
|
||||||
if message_type != TOX_MESSAGE_TYPE['NORMAL']:
|
|
||||||
self.name.setStyleSheet("QLabel { color: #5CB3FF; }")
|
|
||||||
self.message.setAlignment(QtCore.Qt.AlignCenter)
|
|
||||||
self.time.setStyleSheet("QLabel { color: #5CB3FF; }")
|
|
||||||
self.message.setGeometry(QtCore.QRect(100, 0, parent.width() - 160, self.message.height()))
|
|
||||||
self.setFixedHeight(self.message.height())
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
|
||||||
if event.button() == QtCore.Qt.RightButton and event.x() > self.time.x():
|
|
||||||
self.listMenu = QtWidgets.QMenu()
|
|
||||||
delete_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Delete message'))
|
|
||||||
delete_item.triggered.connect(self.delete)
|
|
||||||
parent_position = self.time.mapToGlobal(QtCore.QPoint(0, 0))
|
|
||||||
self.listMenu.move(parent_position)
|
|
||||||
self.listMenu.show()
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
pr = profile.Profile.get_instance()
|
|
||||||
pr.delete_message(self._time)
|
|
||||||
|
|
||||||
def mark_as_sent(self):
|
|
||||||
if self.t:
|
|
||||||
self.time.setText(convert_time(self._time))
|
|
||||||
self.t = False
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def set_avatar(self, pixmap):
|
|
||||||
self.name.setAlignment(QtCore.Qt.AlignCenter)
|
|
||||||
self.message.setAlignment(QtCore.Qt.AlignVCenter)
|
|
||||||
self.setFixedHeight(max(self.height(), 36))
|
|
||||||
self.name.setFixedHeight(self.height())
|
|
||||||
self.message.setFixedHeight(self.height())
|
|
||||||
self.name.setPixmap(pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
|
|
||||||
|
|
||||||
def select_text(self, text):
|
|
||||||
tmp = self.message.toHtml()
|
|
||||||
text = h.escape(text)
|
|
||||||
strings = re.findall(text, tmp, flags=re.IGNORECASE)
|
|
||||||
for s in strings:
|
|
||||||
tmp = self.replace_all(tmp, s)
|
|
||||||
self.message.setHtml(tmp)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def replace_all(text, substring):
|
|
||||||
i, l = 0, len(substring)
|
|
||||||
while i < len(text) - l + 1:
|
|
||||||
index = text[i:].find(substring)
|
|
||||||
if index == -1:
|
|
||||||
break
|
|
||||||
i += index
|
|
||||||
lgt, rgt = text[i:].find('<'), text[i:].find('>')
|
|
||||||
if rgt < lgt:
|
|
||||||
i += rgt + 1
|
|
||||||
continue
|
|
||||||
sub = '<font color="red"><b>{}</b></font>'.format(substring)
|
|
||||||
text = text[:i] + sub + text[i + l:]
|
|
||||||
i += len(sub)
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
class ContactItem(QtWidgets.QWidget):
|
|
||||||
"""
|
|
||||||
Contact in friends list
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
QtWidgets.QWidget.__init__(self, parent)
|
|
||||||
mode = settings.Settings.get_instance()['compact_mode']
|
|
||||||
self.setBaseSize(QtCore.QSize(250, 40 if mode else 70))
|
|
||||||
self.avatar_label = QtWidgets.QLabel(self)
|
|
||||||
size = 32 if mode else 64
|
|
||||||
self.avatar_label.setGeometry(QtCore.QRect(3, 4, size, size))
|
|
||||||
self.avatar_label.setScaledContents(False)
|
|
||||||
self.avatar_label.setAlignment(QtCore.Qt.AlignCenter)
|
|
||||||
self.name = DataLabel(self)
|
|
||||||
self.name.setGeometry(QtCore.QRect(50 if mode else 75, 3 if mode else 10, 150, 15 if mode else 25))
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setFamily(settings.Settings.get_instance()['font'])
|
|
||||||
font.setPointSize(10 if mode else 12)
|
|
||||||
font.setBold(True)
|
|
||||||
self.name.setFont(font)
|
|
||||||
self.status_message = DataLabel(self)
|
|
||||||
self.status_message.setGeometry(QtCore.QRect(50 if mode else 75, 20 if mode else 30, 170, 15 if mode else 20))
|
|
||||||
font.setPointSize(10)
|
|
||||||
font.setBold(False)
|
|
||||||
self.status_message.setFont(font)
|
|
||||||
self.connection_status = StatusCircle(self)
|
|
||||||
self.connection_status.setGeometry(QtCore.QRect(230, -2 if mode else 5, 32, 32))
|
|
||||||
self.messages = UnreadMessagesCount(self)
|
|
||||||
self.messages.setGeometry(QtCore.QRect(20 if mode else 52, 20 if mode else 50, 30, 20))
|
|
||||||
|
|
||||||
|
|
||||||
class StatusCircle(QtWidgets.QWidget):
|
|
||||||
"""
|
|
||||||
Connection status
|
|
||||||
"""
|
|
||||||
def __init__(self, parent):
|
|
||||||
QtWidgets.QWidget.__init__(self, parent)
|
|
||||||
self.setGeometry(0, 0, 32, 32)
|
|
||||||
self.label = QtWidgets.QLabel(self)
|
|
||||||
self.label.setGeometry(QtCore.QRect(0, 0, 32, 32))
|
|
||||||
self.unread = False
|
|
||||||
|
|
||||||
def update(self, status, unread_messages=None):
|
|
||||||
if unread_messages is None:
|
|
||||||
unread_messages = self.unread
|
|
||||||
else:
|
|
||||||
self.unread = unread_messages
|
|
||||||
if status == TOX_USER_STATUS['NONE']:
|
|
||||||
name = 'online'
|
|
||||||
elif status == TOX_USER_STATUS['AWAY']:
|
|
||||||
name = 'idle'
|
|
||||||
elif status == TOX_USER_STATUS['BUSY']:
|
|
||||||
name = 'busy'
|
|
||||||
else:
|
|
||||||
name = 'offline'
|
|
||||||
if unread_messages:
|
|
||||||
name += '_notification'
|
|
||||||
self.label.setGeometry(QtCore.QRect(0, 0, 32, 32))
|
|
||||||
else:
|
|
||||||
self.label.setGeometry(QtCore.QRect(2, 0, 32, 32))
|
|
||||||
pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(name))
|
|
||||||
self.label.setPixmap(pixmap)
|
|
||||||
|
|
||||||
|
|
||||||
class UnreadMessagesCount(QtWidgets.QWidget):
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super(UnreadMessagesCount, self).__init__(parent)
|
|
||||||
self.resize(30, 20)
|
|
||||||
self.label = QtWidgets.QLabel(self)
|
|
||||||
self.label.setGeometry(QtCore.QRect(0, 0, 30, 20))
|
|
||||||
self.label.setVisible(False)
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setFamily(settings.Settings.get_instance()['font'])
|
|
||||||
font.setPointSize(12)
|
|
||||||
font.setBold(True)
|
|
||||||
self.label.setFont(font)
|
|
||||||
self.label.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignCenter)
|
|
||||||
color = settings.Settings.get_instance()['unread_color']
|
|
||||||
self.label.setStyleSheet('QLabel { color: white; background-color: ' + color + '; border-radius: 10; }')
|
|
||||||
|
|
||||||
def update(self, messages_count):
|
|
||||||
color = settings.Settings.get_instance()['unread_color']
|
|
||||||
self.label.setStyleSheet('QLabel { color: white; background-color: ' + color + '; border-radius: 10; }')
|
|
||||||
if messages_count:
|
|
||||||
self.label.setVisible(True)
|
|
||||||
self.label.setText(str(messages_count))
|
|
||||||
else:
|
|
||||||
self.label.setVisible(False)
|
|
||||||
|
|
||||||
|
|
||||||
class FileTransferItem(QtWidgets.QListWidget):
|
|
||||||
|
|
||||||
def __init__(self, file_name, size, time, user, friend_number, file_number, state, width, parent=None):
|
|
||||||
|
|
||||||
QtWidgets.QListWidget.__init__(self, parent)
|
|
||||||
self.resize(QtCore.QSize(width, 34))
|
|
||||||
if state == TOX_FILE_TRANSFER_STATE['CANCELLED']:
|
|
||||||
self.setStyleSheet('QListWidget { border: 1px solid #B40404; }')
|
|
||||||
elif state in PAUSED_FILE_TRANSFERS:
|
|
||||||
self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }')
|
|
||||||
else:
|
|
||||||
self.setStyleSheet('QListWidget { border: 1px solid green; }')
|
|
||||||
self.state = state
|
|
||||||
|
|
||||||
self.name = DataLabel(self)
|
|
||||||
self.name.setGeometry(QtCore.QRect(3, 7, 95, 25))
|
|
||||||
self.name.setTextFormat(QtCore.Qt.PlainText)
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setFamily(settings.Settings.get_instance()['font'])
|
|
||||||
font.setPointSize(11)
|
|
||||||
font.setBold(True)
|
|
||||||
self.name.setFont(font)
|
|
||||||
self.name.setText(user)
|
|
||||||
|
|
||||||
self.time = QtWidgets.QLabel(self)
|
|
||||||
self.time.setGeometry(QtCore.QRect(width - 60, 7, 50, 25))
|
|
||||||
font.setPointSize(10)
|
|
||||||
font.setBold(False)
|
|
||||||
self.time.setFont(font)
|
|
||||||
self.time.setText(convert_time(time))
|
|
||||||
|
|
||||||
self.cancel = QtWidgets.QPushButton(self)
|
|
||||||
self.cancel.setGeometry(QtCore.QRect(width - 125, 2, 30, 30))
|
|
||||||
pixmap = QtGui.QPixmap(curr_directory() + '/images/decline.png')
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.cancel.setIcon(icon)
|
|
||||||
self.cancel.setIconSize(QtCore.QSize(30, 30))
|
|
||||||
self.cancel.setVisible(state in ACTIVE_FILE_TRANSFERS)
|
|
||||||
self.cancel.clicked.connect(lambda: self.cancel_transfer(friend_number, file_number))
|
|
||||||
self.cancel.setStyleSheet('QPushButton:hover { border: 1px solid #3A3939; background-color: none;}')
|
|
||||||
|
|
||||||
self.accept_or_pause = QtWidgets.QPushButton(self)
|
|
||||||
self.accept_or_pause.setGeometry(QtCore.QRect(width - 170, 2, 30, 30))
|
|
||||||
if state == TOX_FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']:
|
|
||||||
self.accept_or_pause.setVisible(True)
|
|
||||||
self.button_update('accept')
|
|
||||||
elif state in DO_NOT_SHOW_ACCEPT_BUTTON:
|
|
||||||
self.accept_or_pause.setVisible(False)
|
|
||||||
elif state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER']: # setup for continue
|
|
||||||
self.accept_or_pause.setVisible(True)
|
|
||||||
self.button_update('resume')
|
|
||||||
else: # pause
|
|
||||||
self.accept_or_pause.setVisible(True)
|
|
||||||
self.button_update('pause')
|
|
||||||
self.accept_or_pause.clicked.connect(lambda: self.accept_or_pause_transfer(friend_number, file_number, size))
|
|
||||||
|
|
||||||
self.accept_or_pause.setStyleSheet('QPushButton:hover { border: 1px solid #3A3939; background-color: none}')
|
|
||||||
|
|
||||||
self.pb = QtWidgets.QProgressBar(self)
|
|
||||||
self.pb.setGeometry(QtCore.QRect(100, 7, 100, 20))
|
|
||||||
self.pb.setValue(0)
|
|
||||||
self.pb.setStyleSheet('QProgressBar { background-color: #302F2F; }')
|
|
||||||
self.pb.setVisible(state in SHOW_PROGRESS_BAR)
|
|
||||||
|
|
||||||
self.file_name = DataLabel(self)
|
|
||||||
self.file_name.setGeometry(QtCore.QRect(210, 7, width - 420, 20))
|
|
||||||
font.setPointSize(12)
|
|
||||||
self.file_name.setFont(font)
|
|
||||||
file_size = size // 1024
|
|
||||||
if not file_size:
|
|
||||||
file_size = '{}B'.format(size)
|
|
||||||
elif file_size >= 1024:
|
|
||||||
file_size = '{}MB'.format(file_size // 1024)
|
|
||||||
else:
|
|
||||||
file_size = '{}KB'.format(file_size)
|
|
||||||
file_data = '{} {}'.format(file_size, file_name)
|
|
||||||
self.file_name.setText(file_data)
|
|
||||||
self.file_name.setToolTip(file_name)
|
|
||||||
self.saved_name = file_name
|
|
||||||
self.time_left = QtWidgets.QLabel(self)
|
|
||||||
self.time_left.setGeometry(QtCore.QRect(width - 92, 7, 30, 20))
|
|
||||||
font.setPointSize(10)
|
|
||||||
self.time_left.setFont(font)
|
|
||||||
self.time_left.setVisible(state == TOX_FILE_TRANSFER_STATE['RUNNING'])
|
|
||||||
self.setFocusPolicy(QtCore.Qt.NoFocus)
|
|
||||||
self.paused = False
|
|
||||||
|
|
||||||
def cancel_transfer(self, friend_number, file_number):
|
|
||||||
pr = profile.Profile.get_instance()
|
|
||||||
pr.cancel_transfer(friend_number, file_number)
|
|
||||||
self.setStyleSheet('QListWidget { border: 1px solid #B40404; }')
|
|
||||||
self.cancel.setVisible(False)
|
|
||||||
self.accept_or_pause.setVisible(False)
|
|
||||||
self.pb.setVisible(False)
|
|
||||||
|
|
||||||
def accept_or_pause_transfer(self, friend_number, file_number, size):
|
|
||||||
if self.state == TOX_FILE_TRANSFER_STATE['INCOMING_NOT_STARTED']:
|
|
||||||
directory = QtWidgets.QFileDialog.getExistingDirectory(self,
|
|
||||||
QtWidgets.QApplication.translate("MainWindow", 'Choose folder'),
|
|
||||||
curr_directory(),
|
|
||||||
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
|
|
||||||
self.pb.setVisible(True)
|
|
||||||
if directory:
|
|
||||||
pr = profile.Profile.get_instance()
|
|
||||||
pr.accept_transfer(self, directory + '/' + self.saved_name, friend_number, file_number, size)
|
|
||||||
self.button_update('pause')
|
|
||||||
elif self.state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER']: # resume
|
|
||||||
self.paused = False
|
|
||||||
profile.Profile.get_instance().resume_transfer(friend_number, file_number)
|
|
||||||
self.button_update('pause')
|
|
||||||
self.state = TOX_FILE_TRANSFER_STATE['RUNNING']
|
|
||||||
else: # pause
|
|
||||||
self.paused = True
|
|
||||||
self.state = TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER']
|
|
||||||
profile.Profile.get_instance().pause_transfer(friend_number, file_number)
|
|
||||||
self.button_update('resume')
|
|
||||||
self.accept_or_pause.clearFocus()
|
|
||||||
|
|
||||||
def button_update(self, path):
|
|
||||||
pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(path))
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.accept_or_pause.setIcon(icon)
|
|
||||||
self.accept_or_pause.setIconSize(QtCore.QSize(30, 30))
|
|
||||||
|
|
||||||
def update_transfer_state(self, state, progress, time):
|
|
||||||
self.pb.setValue(int(progress * 100))
|
|
||||||
if time + 1:
|
|
||||||
m, s = divmod(time, 60)
|
|
||||||
self.time_left.setText('{0:02d}:{1:02d}'.format(m, s))
|
|
||||||
if self.state != state and self.state in ACTIVE_FILE_TRANSFERS:
|
|
||||||
if state == TOX_FILE_TRANSFER_STATE['CANCELLED']:
|
|
||||||
self.setStyleSheet('QListWidget { border: 1px solid #B40404; }')
|
|
||||||
self.cancel.setVisible(False)
|
|
||||||
self.accept_or_pause.setVisible(False)
|
|
||||||
self.pb.setVisible(False)
|
|
||||||
self.state = state
|
|
||||||
self.time_left.setVisible(False)
|
|
||||||
elif state == TOX_FILE_TRANSFER_STATE['FINISHED']:
|
|
||||||
self.accept_or_pause.setVisible(False)
|
|
||||||
self.pb.setVisible(False)
|
|
||||||
self.cancel.setVisible(False)
|
|
||||||
self.setStyleSheet('QListWidget { border: 1px solid green; }')
|
|
||||||
self.state = state
|
|
||||||
self.time_left.setVisible(False)
|
|
||||||
elif state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND']:
|
|
||||||
self.accept_or_pause.setVisible(False)
|
|
||||||
self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }')
|
|
||||||
self.state = state
|
|
||||||
self.time_left.setVisible(False)
|
|
||||||
elif state == TOX_FILE_TRANSFER_STATE['PAUSED_BY_USER']:
|
|
||||||
self.button_update('resume') # setup button continue
|
|
||||||
self.setStyleSheet('QListWidget { border: 1px solid green; }')
|
|
||||||
self.state = state
|
|
||||||
self.time_left.setVisible(False)
|
|
||||||
elif state == TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED']:
|
|
||||||
self.setStyleSheet('QListWidget { border: 1px solid #FF8000; }')
|
|
||||||
self.accept_or_pause.setVisible(False)
|
|
||||||
self.time_left.setVisible(False)
|
|
||||||
self.pb.setVisible(False)
|
|
||||||
elif not self.paused: # active
|
|
||||||
self.pb.setVisible(True)
|
|
||||||
self.accept_or_pause.setVisible(True) # setup to pause
|
|
||||||
self.button_update('pause')
|
|
||||||
self.setStyleSheet('QListWidget { border: 1px solid green; }')
|
|
||||||
self.state = state
|
|
||||||
self.time_left.setVisible(True)
|
|
||||||
|
|
||||||
def mark_as_sent(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class UnsentFileItem(FileTransferItem):
|
|
||||||
|
|
||||||
def __init__(self, file_name, size, user, time, width, parent=None):
|
|
||||||
super(UnsentFileItem, self).__init__(file_name, size, time, user, -1, -1,
|
|
||||||
TOX_FILE_TRANSFER_STATE['PAUSED_BY_FRIEND'], width, parent)
|
|
||||||
self._time = time
|
|
||||||
self.pb.setVisible(False)
|
|
||||||
movie = QtGui.QMovie(curr_directory() + '/images/spinner.gif')
|
|
||||||
self.time.setMovie(movie)
|
|
||||||
movie.start()
|
|
||||||
|
|
||||||
def cancel_transfer(self, *args):
|
|
||||||
pr = profile.Profile.get_instance()
|
|
||||||
pr.cancel_not_started_transfer(self._time)
|
|
||||||
|
|
||||||
|
|
||||||
class InlineImageItem(QtWidgets.QScrollArea):
|
|
||||||
|
|
||||||
def __init__(self, data, width, elem):
|
|
||||||
|
|
||||||
QtWidgets.QScrollArea.__init__(self)
|
|
||||||
self.setFocusPolicy(QtCore.Qt.NoFocus)
|
|
||||||
self._elem = elem
|
|
||||||
self._image_label = QtWidgets.QLabel(self)
|
|
||||||
self._image_label.raise_()
|
|
||||||
self.setWidget(self._image_label)
|
|
||||||
self._image_label.setScaledContents(False)
|
|
||||||
self._pixmap = QtGui.QPixmap()
|
|
||||||
self._pixmap.loadFromData(data, 'PNG')
|
|
||||||
self._max_size = width - 30
|
|
||||||
self._resize_needed = not (self._pixmap.width() <= self._max_size)
|
|
||||||
self._full_size = not self._resize_needed
|
|
||||||
if not self._resize_needed:
|
|
||||||
self._image_label.setPixmap(self._pixmap)
|
|
||||||
self.resize(QtCore.QSize(self._max_size + 5, self._pixmap.height() + 5))
|
|
||||||
self._image_label.setGeometry(5, 0, self._pixmap.width(), self._pixmap.height())
|
|
||||||
else:
|
|
||||||
pixmap = self._pixmap.scaled(self._max_size, self._max_size, QtCore.Qt.KeepAspectRatio)
|
|
||||||
self._image_label.setPixmap(pixmap)
|
|
||||||
self.resize(QtCore.QSize(self._max_size + 5, pixmap.height()))
|
|
||||||
self._image_label.setGeometry(5, 0, self._max_size + 5, pixmap.height())
|
|
||||||
self._elem.setSizeHint(QtCore.QSize(self.width(), self.height()))
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
|
||||||
if event.button() == QtCore.Qt.LeftButton and self._resize_needed: # scale inline
|
|
||||||
if self._full_size:
|
|
||||||
pixmap = self._pixmap.scaled(self._max_size, self._max_size, QtCore.Qt.KeepAspectRatio)
|
|
||||||
self._image_label.setPixmap(pixmap)
|
|
||||||
self.resize(QtCore.QSize(self._max_size, pixmap.height()))
|
|
||||||
self._image_label.setGeometry(5, 0, pixmap.width(), pixmap.height())
|
|
||||||
else:
|
|
||||||
self._image_label.setPixmap(self._pixmap)
|
|
||||||
self.resize(QtCore.QSize(self._max_size, self._pixmap.height() + 17))
|
|
||||||
self._image_label.setGeometry(5, 0, self._pixmap.width(), self._pixmap.height())
|
|
||||||
self._full_size = not self._full_size
|
|
||||||
self._elem.setSizeHint(QtCore.QSize(self.width(), self.height()))
|
|
||||||
elif event.button() == QtCore.Qt.RightButton: # save inline
|
|
||||||
directory = QtWidgets.QFileDialog.getExistingDirectory(self,
|
|
||||||
QtWidgets.QApplication.translate("MainWindow",
|
|
||||||
'Choose folder'),
|
|
||||||
curr_directory(),
|
|
||||||
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
|
|
||||||
if directory:
|
|
||||||
fl = QtCore.QFile(directory + '/toxygen_inline_' + curr_time().replace(':', '_') + '.png')
|
|
||||||
self._pixmap.save(fl, 'PNG')
|
|
||||||
|
|
||||||
def mark_as_sent(self):
|
|
||||||
return False
|
|
|
@ -1,103 +0,0 @@
|
||||||
from PyQt5 import QtWidgets, QtCore
|
|
||||||
from widgets import *
|
|
||||||
|
|
||||||
|
|
||||||
class NickEdit(LineEdit):
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
super(NickEdit, self).__init__(parent)
|
|
||||||
self.parent = parent
|
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
|
||||||
if event.key() == QtCore.Qt.Key_Return:
|
|
||||||
self.parent.create_profile()
|
|
||||||
else:
|
|
||||||
super(NickEdit, self).keyPressEvent(event)
|
|
||||||
|
|
||||||
|
|
||||||
class LoginScreen(CenteredWidget):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(LoginScreen, self).__init__()
|
|
||||||
self.initUI()
|
|
||||||
self.center()
|
|
||||||
|
|
||||||
def initUI(self):
|
|
||||||
self.resize(400, 200)
|
|
||||||
self.setMinimumSize(QtCore.QSize(400, 200))
|
|
||||||
self.setMaximumSize(QtCore.QSize(400, 200))
|
|
||||||
self.new_profile = QtWidgets.QPushButton(self)
|
|
||||||
self.new_profile.setGeometry(QtCore.QRect(20, 150, 171, 27))
|
|
||||||
self.new_profile.clicked.connect(self.create_profile)
|
|
||||||
self.label = QtWidgets.QLabel(self)
|
|
||||||
self.label.setGeometry(QtCore.QRect(20, 70, 101, 17))
|
|
||||||
self.new_name = NickEdit(self)
|
|
||||||
self.new_name.setGeometry(QtCore.QRect(20, 100, 171, 31))
|
|
||||||
self.load_profile = QtWidgets.QPushButton(self)
|
|
||||||
self.load_profile.setGeometry(QtCore.QRect(220, 150, 161, 27))
|
|
||||||
self.load_profile.clicked.connect(self.load_ex_profile)
|
|
||||||
self.default = QtWidgets.QCheckBox(self)
|
|
||||||
self.default.setGeometry(QtCore.QRect(220, 110, 131, 22))
|
|
||||||
self.groupBox = QtWidgets.QGroupBox(self)
|
|
||||||
self.groupBox.setGeometry(QtCore.QRect(210, 40, 181, 151))
|
|
||||||
self.comboBox = QtWidgets.QComboBox(self.groupBox)
|
|
||||||
self.comboBox.setGeometry(QtCore.QRect(10, 30, 161, 27))
|
|
||||||
self.groupBox_2 = QtWidgets.QGroupBox(self)
|
|
||||||
self.groupBox_2.setGeometry(QtCore.QRect(10, 40, 191, 151))
|
|
||||||
self.toxygen = QtWidgets.QLabel(self)
|
|
||||||
self.groupBox.raise_()
|
|
||||||
self.groupBox_2.raise_()
|
|
||||||
self.comboBox.raise_()
|
|
||||||
self.default.raise_()
|
|
||||||
self.load_profile.raise_()
|
|
||||||
self.new_name.raise_()
|
|
||||||
self.new_profile.raise_()
|
|
||||||
self.toxygen.setGeometry(QtCore.QRect(160, 8, 90, 25))
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setFamily("Impact")
|
|
||||||
font.setPointSize(16)
|
|
||||||
self.toxygen.setFont(font)
|
|
||||||
self.toxygen.setObjectName("toxygen")
|
|
||||||
self.type = 0
|
|
||||||
self.number = -1
|
|
||||||
self.load_as_default = False
|
|
||||||
self.name = None
|
|
||||||
self.retranslateUi()
|
|
||||||
QtCore.QMetaObject.connectSlotsByName(self)
|
|
||||||
|
|
||||||
def retranslateUi(self):
|
|
||||||
self.new_name.setPlaceholderText(QtWidgets.QApplication.translate("login", "Profile name"))
|
|
||||||
self.setWindowTitle(QtWidgets.QApplication.translate("login", "Log in"))
|
|
||||||
self.new_profile.setText(QtWidgets.QApplication.translate("login", "Create"))
|
|
||||||
self.label.setText(QtWidgets.QApplication.translate("login", "Profile name:"))
|
|
||||||
self.load_profile.setText(QtWidgets.QApplication.translate("login", "Load profile"))
|
|
||||||
self.default.setText(QtWidgets.QApplication.translate("login", "Use as default"))
|
|
||||||
self.groupBox.setTitle(QtWidgets.QApplication.translate("login", "Load existing profile"))
|
|
||||||
self.groupBox_2.setTitle(QtWidgets.QApplication.translate("login", "Create new profile"))
|
|
||||||
self.toxygen.setText(QtWidgets.QApplication.translate("login", "toxygen"))
|
|
||||||
|
|
||||||
def create_profile(self):
|
|
||||||
self.type = 1
|
|
||||||
self.name = self.new_name.text()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def load_ex_profile(self):
|
|
||||||
if not self.create_only:
|
|
||||||
self.type = 2
|
|
||||||
self.number = self.comboBox.currentIndex()
|
|
||||||
self.load_as_default = self.default.isChecked()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def update_select(self, data):
|
|
||||||
list_of_profiles = []
|
|
||||||
for elem in data:
|
|
||||||
list_of_profiles.append(elem)
|
|
||||||
self.comboBox.addItems(list_of_profiles)
|
|
||||||
self.create_only = not list_of_profiles
|
|
||||||
|
|
||||||
def update_on_close(self, func):
|
|
||||||
self.onclose = func
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
|
||||||
self.onclose(self.type, self.number, self.load_as_default, self.name)
|
|
||||||
event.accept()
|
|
|
@ -1,757 +0,0 @@
|
||||||
from menu import *
|
|
||||||
from profile import *
|
|
||||||
from list_items import *
|
|
||||||
from widgets import MultilineEdit, ComboBox
|
|
||||||
import plugin_support
|
|
||||||
from mainscreen_widgets import *
|
|
||||||
import settings
|
|
||||||
import toxes
|
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QtWidgets.QMainWindow, Singleton):
|
|
||||||
|
|
||||||
def __init__(self, tox, reset, tray):
|
|
||||||
super().__init__()
|
|
||||||
Singleton.__init__(self)
|
|
||||||
self.reset = reset
|
|
||||||
self.tray = tray
|
|
||||||
self.setAcceptDrops(True)
|
|
||||||
self.initUI(tox)
|
|
||||||
self._saved = False
|
|
||||||
if settings.Settings.get_instance()['show_welcome_screen']:
|
|
||||||
self.ws = WelcomeScreen()
|
|
||||||
|
|
||||||
def setup_menu(self, window):
|
|
||||||
self.menubar = QtWidgets.QMenuBar(window)
|
|
||||||
self.menubar.setObjectName("menubar")
|
|
||||||
self.menubar.setNativeMenuBar(False)
|
|
||||||
self.menubar.setMinimumSize(self.width(), 25)
|
|
||||||
self.menubar.setMaximumSize(self.width(), 25)
|
|
||||||
self.menubar.setBaseSize(self.width(), 25)
|
|
||||||
self.menuProfile = QtWidgets.QMenu(self.menubar)
|
|
||||||
|
|
||||||
self.menuProfile = QtWidgets.QMenu(self.menubar)
|
|
||||||
self.menuProfile.setObjectName("menuProfile")
|
|
||||||
self.menuSettings = QtWidgets.QMenu(self.menubar)
|
|
||||||
self.menuSettings.setObjectName("menuSettings")
|
|
||||||
self.menuPlugins = QtWidgets.QMenu(self.menubar)
|
|
||||||
self.menuPlugins.setObjectName("menuPlugins")
|
|
||||||
self.menuAbout = QtWidgets.QMenu(self.menubar)
|
|
||||||
self.menuAbout.setObjectName("menuAbout")
|
|
||||||
|
|
||||||
self.actionAdd_friend = QtWidgets.QAction(window)
|
|
||||||
self.actionAdd_gc = QtWidgets.QAction(window)
|
|
||||||
self.actionAdd_friend.setObjectName("actionAdd_friend")
|
|
||||||
self.actionprofilesettings = QtWidgets.QAction(window)
|
|
||||||
self.actionprofilesettings.setObjectName("actionprofilesettings")
|
|
||||||
self.actionPrivacy_settings = QtWidgets.QAction(window)
|
|
||||||
self.actionPrivacy_settings.setObjectName("actionPrivacy_settings")
|
|
||||||
self.actionInterface_settings = QtWidgets.QAction(window)
|
|
||||||
self.actionInterface_settings.setObjectName("actionInterface_settings")
|
|
||||||
self.actionNotifications = QtWidgets.QAction(window)
|
|
||||||
self.actionNotifications.setObjectName("actionNotifications")
|
|
||||||
self.actionNetwork = QtWidgets.QAction(window)
|
|
||||||
self.actionNetwork.setObjectName("actionNetwork")
|
|
||||||
self.actionAbout_program = QtWidgets.QAction(window)
|
|
||||||
self.actionAbout_program.setObjectName("actionAbout_program")
|
|
||||||
self.updateSettings = QtWidgets.QAction(window)
|
|
||||||
self.actionSettings = QtWidgets.QAction(window)
|
|
||||||
self.actionSettings.setObjectName("actionSettings")
|
|
||||||
self.audioSettings = QtWidgets.QAction(window)
|
|
||||||
self.videoSettings = QtWidgets.QAction(window)
|
|
||||||
self.pluginData = QtWidgets.QAction(window)
|
|
||||||
self.importPlugin = QtWidgets.QAction(window)
|
|
||||||
self.reloadPlugins = QtWidgets.QAction(window)
|
|
||||||
self.lockApp = QtWidgets.QAction(window)
|
|
||||||
self.menuProfile.addAction(self.actionAdd_friend)
|
|
||||||
self.menuProfile.addAction(self.actionAdd_gc)
|
|
||||||
self.menuProfile.addAction(self.actionSettings)
|
|
||||||
self.menuProfile.addAction(self.lockApp)
|
|
||||||
self.menuSettings.addAction(self.actionPrivacy_settings)
|
|
||||||
self.menuSettings.addAction(self.actionInterface_settings)
|
|
||||||
self.menuSettings.addAction(self.actionNotifications)
|
|
||||||
self.menuSettings.addAction(self.actionNetwork)
|
|
||||||
self.menuSettings.addAction(self.audioSettings)
|
|
||||||
self.menuSettings.addAction(self.videoSettings)
|
|
||||||
self.menuSettings.addAction(self.updateSettings)
|
|
||||||
self.menuPlugins.addAction(self.pluginData)
|
|
||||||
self.menuPlugins.addAction(self.importPlugin)
|
|
||||||
self.menuPlugins.addAction(self.reloadPlugins)
|
|
||||||
self.menuAbout.addAction(self.actionAbout_program)
|
|
||||||
|
|
||||||
self.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)
|
|
||||||
self.actionNetwork.triggered.connect(self.network_settings)
|
|
||||||
self.actionAdd_friend.triggered.connect(self.add_contact)
|
|
||||||
self.actionAdd_gc.triggered.connect(self.create_gc)
|
|
||||||
self.actionSettings.triggered.connect(self.profile_settings)
|
|
||||||
self.actionPrivacy_settings.triggered.connect(self.privacy_settings)
|
|
||||||
self.actionInterface_settings.triggered.connect(self.interface_settings)
|
|
||||||
self.actionNotifications.triggered.connect(self.notification_settings)
|
|
||||||
self.audioSettings.triggered.connect(self.audio_settings)
|
|
||||||
self.videoSettings.triggered.connect(self.video_settings)
|
|
||||||
self.updateSettings.triggered.connect(self.update_settings)
|
|
||||||
self.pluginData.triggered.connect(self.plugins_menu)
|
|
||||||
self.lockApp.triggered.connect(self.lock_app)
|
|
||||||
self.importPlugin.triggered.connect(self.import_plugin)
|
|
||||||
self.reloadPlugins.triggered.connect(self.reload_plugins)
|
|
||||||
|
|
||||||
def languageChange(self, *args, **kwargs):
|
|
||||||
self.retranslateUi()
|
|
||||||
|
|
||||||
def event(self, event):
|
|
||||||
if event.type() == QtCore.QEvent.WindowActivate:
|
|
||||||
self.tray.setIcon(QtGui.QIcon(curr_directory() + '/images/icon.png'))
|
|
||||||
self.messages.repaint()
|
|
||||||
return super(MainWindow, self).event(event)
|
|
||||||
|
|
||||||
def retranslateUi(self):
|
|
||||||
self.lockApp.setText(QtWidgets.QApplication.translate("MainWindow", "Lock"))
|
|
||||||
self.menuPlugins.setTitle(QtWidgets.QApplication.translate("MainWindow", "Plugins"))
|
|
||||||
self.pluginData.setText(QtWidgets.QApplication.translate("MainWindow", "List of plugins"))
|
|
||||||
self.menuProfile.setTitle(QtWidgets.QApplication.translate("MainWindow", "Profile"))
|
|
||||||
self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings"))
|
|
||||||
self.menuAbout.setTitle(QtWidgets.QApplication.translate("MainWindow", "About"))
|
|
||||||
self.actionAdd_friend.setText(QtWidgets.QApplication.translate("MainWindow", "Add contact"))
|
|
||||||
self.actionAdd_gc.setText(QtWidgets.QApplication.translate("MainWindow", "Create group chat"))
|
|
||||||
self.actionprofilesettings.setText(QtWidgets.QApplication.translate("MainWindow", "Profile"))
|
|
||||||
self.actionPrivacy_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Privacy"))
|
|
||||||
self.actionInterface_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Interface"))
|
|
||||||
self.actionNotifications.setText(QtWidgets.QApplication.translate("MainWindow", "Notifications"))
|
|
||||||
self.actionNetwork.setText(QtWidgets.QApplication.translate("MainWindow", "Network"))
|
|
||||||
self.actionAbout_program.setText(QtWidgets.QApplication.translate("MainWindow", "About program"))
|
|
||||||
self.actionSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Settings"))
|
|
||||||
self.audioSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Audio"))
|
|
||||||
self.videoSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Video"))
|
|
||||||
self.updateSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Updates"))
|
|
||||||
self.contact_name.setPlaceholderText(QtWidgets.QApplication.translate("MainWindow", "Search"))
|
|
||||||
self.sendMessageButton.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Send message"))
|
|
||||||
self.callButton.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Start audio call with friend"))
|
|
||||||
self.online_contacts.clear()
|
|
||||||
self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "All"))
|
|
||||||
self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online"))
|
|
||||||
self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online first"))
|
|
||||||
self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Name"))
|
|
||||||
self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online and by name"))
|
|
||||||
self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online first and by name"))
|
|
||||||
ind = Settings.get_instance()['sorting']
|
|
||||||
d = {0: 0, 1: 1, 2: 2, 3: 4, 1 | 4: 4, 2 | 4: 5}
|
|
||||||
self.online_contacts.setCurrentIndex(d[ind])
|
|
||||||
self.importPlugin.setText(QtWidgets.QApplication.translate("MainWindow", "Import plugin"))
|
|
||||||
self.reloadPlugins.setText(QtWidgets.QApplication.translate("MainWindow", "Reload plugins"))
|
|
||||||
|
|
||||||
def setup_right_bottom(self, Form):
|
|
||||||
Form.resize(650, 60)
|
|
||||||
self.messageEdit = MessageArea(Form, self)
|
|
||||||
self.messageEdit.setGeometry(QtCore.QRect(0, 3, 450, 55))
|
|
||||||
self.messageEdit.setObjectName("messageEdit")
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setPointSize(11)
|
|
||||||
font.setFamily(settings.Settings.get_instance()['font'])
|
|
||||||
self.messageEdit.setFont(font)
|
|
||||||
|
|
||||||
self.sendMessageButton = QtWidgets.QPushButton(Form)
|
|
||||||
self.sendMessageButton.setGeometry(QtCore.QRect(565, 3, 60, 55))
|
|
||||||
self.sendMessageButton.setObjectName("sendMessageButton")
|
|
||||||
|
|
||||||
self.menuButton = MenuButton(Form, self.show_menu)
|
|
||||||
self.menuButton.setGeometry(QtCore.QRect(QtCore.QRect(455, 3, 55, 55)))
|
|
||||||
|
|
||||||
pixmap = QtGui.QPixmap('send.png')
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.sendMessageButton.setIcon(icon)
|
|
||||||
self.sendMessageButton.setIconSize(QtCore.QSize(45, 60))
|
|
||||||
|
|
||||||
pixmap = QtGui.QPixmap('menu.png')
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.menuButton.setIcon(icon)
|
|
||||||
self.menuButton.setIconSize(QtCore.QSize(40, 40))
|
|
||||||
|
|
||||||
self.sendMessageButton.clicked.connect(self.send_message)
|
|
||||||
|
|
||||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
|
||||||
|
|
||||||
def setup_left_center_menu(self, Form):
|
|
||||||
Form.resize(270, 25)
|
|
||||||
self.search_label = QtWidgets.QLabel(Form)
|
|
||||||
self.search_label.setGeometry(QtCore.QRect(3, 2, 20, 20))
|
|
||||||
pixmap = QtGui.QPixmap()
|
|
||||||
pixmap.load(curr_directory() + '/images/search.png')
|
|
||||||
self.search_label.setScaledContents(False)
|
|
||||||
self.search_label.setPixmap(pixmap)
|
|
||||||
|
|
||||||
self.contact_name = LineEdit(Form)
|
|
||||||
self.contact_name.setGeometry(QtCore.QRect(0, 0, 150, 25))
|
|
||||||
self.contact_name.setObjectName("contact_name")
|
|
||||||
self.contact_name.textChanged.connect(self.filtering)
|
|
||||||
|
|
||||||
self.online_contacts = ComboBox(Form)
|
|
||||||
self.online_contacts.setGeometry(QtCore.QRect(150, 0, 120, 25))
|
|
||||||
self.online_contacts.activated[int].connect(lambda x: self.filtering())
|
|
||||||
self.search_label.raise_()
|
|
||||||
|
|
||||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
|
||||||
|
|
||||||
def setup_left_top(self, Form):
|
|
||||||
Form.setCursor(QtCore.Qt.PointingHandCursor)
|
|
||||||
Form.setMinimumSize(QtCore.QSize(270, 75))
|
|
||||||
Form.setMaximumSize(QtCore.QSize(270, 75))
|
|
||||||
Form.setBaseSize(QtCore.QSize(270, 75))
|
|
||||||
self.avatar_label = Form.avatar_label = QtWidgets.QLabel(Form)
|
|
||||||
self.avatar_label.setGeometry(QtCore.QRect(5, 5, 64, 64))
|
|
||||||
self.avatar_label.setScaledContents(False)
|
|
||||||
self.avatar_label.setAlignment(QtCore.Qt.AlignCenter)
|
|
||||||
self.name = Form.name = DataLabel(Form)
|
|
||||||
Form.name.setGeometry(QtCore.QRect(75, 15, 150, 25))
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setFamily(settings.Settings.get_instance()['font'])
|
|
||||||
font.setPointSize(14)
|
|
||||||
font.setBold(True)
|
|
||||||
Form.name.setFont(font)
|
|
||||||
Form.name.setObjectName("name")
|
|
||||||
self.status_message = Form.status_message = DataLabel(Form)
|
|
||||||
Form.status_message.setGeometry(QtCore.QRect(75, 35, 170, 25))
|
|
||||||
font.setPointSize(12)
|
|
||||||
font.setBold(False)
|
|
||||||
Form.status_message.setFont(font)
|
|
||||||
Form.status_message.setObjectName("status_message")
|
|
||||||
self.connection_status = Form.connection_status = StatusCircle(Form)
|
|
||||||
Form.connection_status.setGeometry(QtCore.QRect(230, 10, 32, 32))
|
|
||||||
self.avatar_label.mouseReleaseEvent = self.profile_settings
|
|
||||||
self.status_message.mouseReleaseEvent = self.profile_settings
|
|
||||||
self.name.mouseReleaseEvent = self.profile_settings
|
|
||||||
self.connection_status.raise_()
|
|
||||||
Form.connection_status.setObjectName("connection_status")
|
|
||||||
|
|
||||||
def setup_right_top(self, Form):
|
|
||||||
Form.resize(650, 75)
|
|
||||||
self.account_avatar = QtWidgets.QLabel(Form)
|
|
||||||
self.account_avatar.setGeometry(QtCore.QRect(10, 5, 64, 64))
|
|
||||||
self.account_avatar.setScaledContents(False)
|
|
||||||
self.account_name = DataLabel(Form)
|
|
||||||
self.account_name.setGeometry(QtCore.QRect(100, 0, 400, 25))
|
|
||||||
self.account_name.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse)
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setFamily(settings.Settings.get_instance()['font'])
|
|
||||||
font.setPointSize(14)
|
|
||||||
font.setBold(True)
|
|
||||||
self.account_name.setFont(font)
|
|
||||||
self.account_name.setObjectName("account_name")
|
|
||||||
self.account_status = DataLabel(Form)
|
|
||||||
self.account_status.setGeometry(QtCore.QRect(100, 20, 400, 25))
|
|
||||||
self.account_status.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse)
|
|
||||||
font.setPointSize(12)
|
|
||||||
font.setBold(False)
|
|
||||||
self.account_status.setFont(font)
|
|
||||||
self.account_status.setObjectName("account_status")
|
|
||||||
self.callButton = QtWidgets.QPushButton(Form)
|
|
||||||
self.callButton.setGeometry(QtCore.QRect(550, 5, 50, 50))
|
|
||||||
self.callButton.setObjectName("callButton")
|
|
||||||
self.callButton.clicked.connect(lambda: self.profile.call_click(True))
|
|
||||||
self.videocallButton = QtWidgets.QPushButton(Form)
|
|
||||||
self.videocallButton.setGeometry(QtCore.QRect(550, 5, 50, 50))
|
|
||||||
self.videocallButton.setObjectName("videocallButton")
|
|
||||||
self.videocallButton.clicked.connect(lambda: self.profile.call_click(True, True))
|
|
||||||
self.update_call_state('call')
|
|
||||||
self.typing = QtWidgets.QLabel(Form)
|
|
||||||
self.typing.setGeometry(QtCore.QRect(500, 25, 50, 30))
|
|
||||||
pixmap = QtGui.QPixmap(QtCore.QSize(50, 30))
|
|
||||||
pixmap.load(curr_directory() + '/images/typing.png')
|
|
||||||
self.typing.setScaledContents(False)
|
|
||||||
self.typing.setPixmap(pixmap.scaled(50, 30, QtCore.Qt.KeepAspectRatio))
|
|
||||||
self.typing.setVisible(False)
|
|
||||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
|
||||||
|
|
||||||
def setup_left_center(self, widget):
|
|
||||||
self.friends_list = QtWidgets.QListWidget(widget)
|
|
||||||
self.friends_list.setObjectName("friends_list")
|
|
||||||
self.friends_list.setGeometry(0, 0, 270, 310)
|
|
||||||
self.friends_list.clicked.connect(self.friend_click)
|
|
||||||
self.friends_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
|
||||||
self.friends_list.customContextMenuRequested.connect(self.friend_right_click)
|
|
||||||
self.friends_list.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
|
||||||
self.friends_list.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
|
|
||||||
self.friends_list.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
||||||
self.friends_list.verticalScrollBar().setContextMenuPolicy(QtCore.Qt.NoContextMenu)
|
|
||||||
|
|
||||||
def setup_right_center(self, widget):
|
|
||||||
self.messages = QtWidgets.QListWidget(widget)
|
|
||||||
self.messages.setGeometry(0, 0, 620, 310)
|
|
||||||
self.messages.setObjectName("messages")
|
|
||||||
self.messages.setSpacing(1)
|
|
||||||
self.messages.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
|
|
||||||
self.messages.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
||||||
self.messages.focusOutEvent = lambda event: self.messages.clearSelection()
|
|
||||||
self.messages.verticalScrollBar().setContextMenuPolicy(QtCore.Qt.NoContextMenu)
|
|
||||||
|
|
||||||
def load(pos):
|
|
||||||
if not pos:
|
|
||||||
self.profile.load_history()
|
|
||||||
self.messages.verticalScrollBar().setValue(1)
|
|
||||||
self.messages.verticalScrollBar().valueChanged.connect(load)
|
|
||||||
self.messages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
|
||||||
self.messages.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
|
||||||
|
|
||||||
def initUI(self, tox):
|
|
||||||
self.setMinimumSize(920, 500)
|
|
||||||
s = Settings.get_instance()
|
|
||||||
self.setGeometry(s['x'], s['y'], s['width'], s['height'])
|
|
||||||
self.setWindowTitle('Toxygen')
|
|
||||||
os.chdir(curr_directory() + '/images/')
|
|
||||||
menu = QtWidgets.QWidget()
|
|
||||||
main = QtWidgets.QWidget()
|
|
||||||
grid = QtWidgets.QGridLayout()
|
|
||||||
search = QtWidgets.QWidget()
|
|
||||||
name = QtWidgets.QWidget()
|
|
||||||
info = QtWidgets.QWidget()
|
|
||||||
main_list = QtWidgets.QWidget()
|
|
||||||
messages = QtWidgets.QWidget()
|
|
||||||
message_buttons = QtWidgets.QWidget()
|
|
||||||
self.setup_left_center_menu(search)
|
|
||||||
self.setup_left_top(name)
|
|
||||||
self.setup_right_center(messages)
|
|
||||||
self.setup_right_top(info)
|
|
||||||
self.setup_right_bottom(message_buttons)
|
|
||||||
self.setup_left_center(main_list)
|
|
||||||
self.setup_menu(menu)
|
|
||||||
if not Settings.get_instance()['mirror_mode']:
|
|
||||||
grid.addWidget(search, 2, 0)
|
|
||||||
grid.addWidget(name, 1, 0)
|
|
||||||
grid.addWidget(messages, 2, 1, 2, 1)
|
|
||||||
grid.addWidget(info, 1, 1)
|
|
||||||
grid.addWidget(message_buttons, 4, 1)
|
|
||||||
grid.addWidget(main_list, 3, 0, 2, 1)
|
|
||||||
grid.setColumnMinimumWidth(1, 500)
|
|
||||||
grid.setColumnMinimumWidth(0, 270)
|
|
||||||
else:
|
|
||||||
grid.addWidget(search, 2, 1)
|
|
||||||
grid.addWidget(name, 1, 1)
|
|
||||||
grid.addWidget(messages, 2, 0, 2, 1)
|
|
||||||
grid.addWidget(info, 1, 0)
|
|
||||||
grid.addWidget(message_buttons, 4, 0)
|
|
||||||
grid.addWidget(main_list, 3, 1, 2, 1)
|
|
||||||
grid.setColumnMinimumWidth(0, 500)
|
|
||||||
grid.setColumnMinimumWidth(1, 270)
|
|
||||||
|
|
||||||
grid.addWidget(menu, 0, 0, 1, 2)
|
|
||||||
grid.setSpacing(0)
|
|
||||||
grid.setContentsMargins(0, 0, 0, 0)
|
|
||||||
grid.setRowMinimumHeight(0, 25)
|
|
||||||
grid.setRowMinimumHeight(1, 75)
|
|
||||||
grid.setRowMinimumHeight(2, 25)
|
|
||||||
grid.setRowMinimumHeight(3, 320)
|
|
||||||
grid.setRowMinimumHeight(4, 55)
|
|
||||||
grid.setColumnStretch(1, 1)
|
|
||||||
grid.setRowStretch(3, 1)
|
|
||||||
main.setLayout(grid)
|
|
||||||
self.setCentralWidget(main)
|
|
||||||
self.messageEdit.setFocus()
|
|
||||||
self.user_info = name
|
|
||||||
self.friend_info = info
|
|
||||||
self.retranslateUi()
|
|
||||||
self.profile = Profile(tox, self)
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
|
||||||
s = Settings.get_instance()
|
|
||||||
if not s['close_to_tray'] or s.closing:
|
|
||||||
if not self._saved:
|
|
||||||
self._saved = True
|
|
||||||
self.profile.save_history()
|
|
||||||
self.profile.close()
|
|
||||||
s['x'] = self.geometry().x()
|
|
||||||
s['y'] = self.geometry().y()
|
|
||||||
s['width'] = self.width()
|
|
||||||
s['height'] = self.height()
|
|
||||||
s.save()
|
|
||||||
QtWidgets.QApplication.closeAllWindows()
|
|
||||||
event.accept()
|
|
||||||
elif QtWidgets.QSystemTrayIcon.isSystemTrayAvailable():
|
|
||||||
event.ignore()
|
|
||||||
self.hide()
|
|
||||||
|
|
||||||
def close_window(self):
|
|
||||||
Settings.get_instance().closing = True
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def resizeEvent(self, *args, **kwargs):
|
|
||||||
self.messages.setGeometry(0, 0, self.width() - 270, self.height() - 155)
|
|
||||||
self.friends_list.setGeometry(0, 0, 270, self.height() - 125)
|
|
||||||
|
|
||||||
self.videocallButton.setGeometry(QtCore.QRect(self.width() - 330, 10, 50, 50))
|
|
||||||
self.callButton.setGeometry(QtCore.QRect(self.width() - 390, 10, 50, 50))
|
|
||||||
self.typing.setGeometry(QtCore.QRect(self.width() - 450, 20, 50, 30))
|
|
||||||
|
|
||||||
self.messageEdit.setGeometry(QtCore.QRect(55, 0, self.width() - 395, 55))
|
|
||||||
self.menuButton.setGeometry(QtCore.QRect(0, 0, 55, 55))
|
|
||||||
self.sendMessageButton.setGeometry(QtCore.QRect(self.width() - 340, 0, 70, 55))
|
|
||||||
|
|
||||||
self.account_name.setGeometry(QtCore.QRect(100, 15, self.width() - 560, 25))
|
|
||||||
self.account_status.setGeometry(QtCore.QRect(100, 35, self.width() - 560, 25))
|
|
||||||
self.messageEdit.setFocus()
|
|
||||||
self.profile.update()
|
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
|
||||||
if event.key() == QtCore.Qt.Key_Escape and QtWidgets.QSystemTrayIcon.isSystemTrayAvailable():
|
|
||||||
self.hide()
|
|
||||||
elif event.key() == QtCore.Qt.Key_C and event.modifiers() & QtCore.Qt.ControlModifier and self.messages.selectedIndexes():
|
|
||||||
rows = list(map(lambda x: self.messages.row(x), self.messages.selectedItems()))
|
|
||||||
indexes = (rows[0] - self.messages.count(), rows[-1] - self.messages.count())
|
|
||||||
s = self.profile.export_history(self.profile.active_friend, True, indexes)
|
|
||||||
clipboard = QtWidgets.QApplication.clipboard()
|
|
||||||
clipboard.setText(s)
|
|
||||||
elif event.key() == QtCore.Qt.Key_Z and event.modifiers() & QtCore.Qt.ControlModifier and self.messages.selectedIndexes():
|
|
||||||
self.messages.clearSelection()
|
|
||||||
elif event.key() == QtCore.Qt.Key_F and event.modifiers() & QtCore.Qt.ControlModifier:
|
|
||||||
self.show_search_field()
|
|
||||||
else:
|
|
||||||
super(MainWindow, self).keyPressEvent(event)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Functions which called when user click in menu
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def about_program(self):
|
|
||||||
import util
|
|
||||||
msgBox = QtWidgets.QMessageBox()
|
|
||||||
msgBox.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "About"))
|
|
||||||
text = (QtWidgets.QApplication.translate("MainWindow", 'Toxygen is Tox client written on Python.<br>Version: '))
|
|
||||||
github = '<br><a href="https://github.com/toxygen-project/toxygen/">Github</a>'
|
|
||||||
submit_a_bug = '<br><a href="https://github.com/toxygen-project/toxygen/issues">Submit a bug</a>'
|
|
||||||
msgBox.setText(text + util.program_version + github + submit_a_bug)
|
|
||||||
msgBox.exec_()
|
|
||||||
|
|
||||||
def network_settings(self):
|
|
||||||
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, link=''):
|
|
||||||
self.a_c = AddContact(link or '')
|
|
||||||
self.a_c.show()
|
|
||||||
|
|
||||||
def create_gc(self):
|
|
||||||
self.profile.create_group_chat()
|
|
||||||
|
|
||||||
def profile_settings(self, *args):
|
|
||||||
self.p_s = ProfileSettings()
|
|
||||||
self.p_s.show()
|
|
||||||
|
|
||||||
def privacy_settings(self):
|
|
||||||
self.priv_s = PrivacySettings()
|
|
||||||
self.priv_s.show()
|
|
||||||
|
|
||||||
def notification_settings(self):
|
|
||||||
self.notif_s = NotificationsSettings()
|
|
||||||
self.notif_s.show()
|
|
||||||
|
|
||||||
def interface_settings(self):
|
|
||||||
self.int_s = InterfaceSettings()
|
|
||||||
self.int_s.show()
|
|
||||||
|
|
||||||
def audio_settings(self):
|
|
||||||
self.audio_s = AudioSettings()
|
|
||||||
self.audio_s.show()
|
|
||||||
|
|
||||||
def video_settings(self):
|
|
||||||
self.video_s = VideoSettings()
|
|
||||||
self.video_s.show()
|
|
||||||
|
|
||||||
def update_settings(self):
|
|
||||||
self.update_s = UpdateSettings()
|
|
||||||
self.update_s.show()
|
|
||||||
|
|
||||||
def reload_plugins(self):
|
|
||||||
plugin_loader = plugin_support.PluginLoader.get_instance()
|
|
||||||
if plugin_loader is not None:
|
|
||||||
plugin_loader.reload()
|
|
||||||
|
|
||||||
def import_plugin(self):
|
|
||||||
import util
|
|
||||||
directory = QtWidgets.QFileDialog.getExistingDirectory(self,
|
|
||||||
QtWidgets.QApplication.translate("MainWindow", 'Choose folder with plugin'),
|
|
||||||
util.curr_directory(),
|
|
||||||
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
|
|
||||||
if directory:
|
|
||||||
src = directory + '/'
|
|
||||||
dest = curr_directory() + '/plugins/'
|
|
||||||
util.copy(src, dest)
|
|
||||||
msgBox = QtWidgets.QMessageBox()
|
|
||||||
msgBox.setWindowTitle(
|
|
||||||
QtWidgets.QApplication.translate("MainWindow", "Restart Toxygen"))
|
|
||||||
msgBox.setText(
|
|
||||||
QtWidgets.QApplication.translate("MainWindow", 'Plugin will be loaded after restart'))
|
|
||||||
msgBox.exec_()
|
|
||||||
|
|
||||||
def lock_app(self):
|
|
||||||
if toxes.ToxES.get_instance().has_password():
|
|
||||||
Settings.get_instance().locked = True
|
|
||||||
self.hide()
|
|
||||||
else:
|
|
||||||
msgBox = QtWidgets.QMessageBox()
|
|
||||||
msgBox.setWindowTitle(
|
|
||||||
QtWidgets.QApplication.translate("MainWindow", "Cannot lock app"))
|
|
||||||
msgBox.setText(
|
|
||||||
QtWidgets.QApplication.translate("MainWindow", 'Error. Profile password is not set.'))
|
|
||||||
msgBox.exec_()
|
|
||||||
|
|
||||||
def show_menu(self):
|
|
||||||
if not hasattr(self, 'menu'):
|
|
||||||
self.menu = DropdownMenu(self)
|
|
||||||
self.menu.setGeometry(QtCore.QRect(0 if Settings.get_instance()['mirror_mode'] else 270,
|
|
||||||
self.height() - 120,
|
|
||||||
180,
|
|
||||||
120))
|
|
||||||
self.menu.show()
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Messages, calls and file transfers
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def send_message(self):
|
|
||||||
text = self.messageEdit.toPlainText()
|
|
||||||
self.profile.send_message(text)
|
|
||||||
|
|
||||||
def send_file(self):
|
|
||||||
self.menu.hide()
|
|
||||||
if self.profile.active_friend + 1and self.profile.is_active_a_friend():
|
|
||||||
choose = QtWidgets.QApplication.translate("MainWindow", 'Choose file')
|
|
||||||
name = QtWidgets.QFileDialog.getOpenFileName(self, choose, options=QtWidgets.QFileDialog.DontUseNativeDialog)
|
|
||||||
if name[0]:
|
|
||||||
self.profile.send_file(name[0])
|
|
||||||
|
|
||||||
def send_screenshot(self, hide=False):
|
|
||||||
self.menu.hide()
|
|
||||||
if self.profile.active_friend + 1 and self.profile.is_active_a_friend():
|
|
||||||
self.sw = ScreenShotWindow(self)
|
|
||||||
self.sw.show()
|
|
||||||
if hide:
|
|
||||||
self.hide()
|
|
||||||
|
|
||||||
def send_smiley(self):
|
|
||||||
self.menu.hide()
|
|
||||||
if self.profile.active_friend + 1:
|
|
||||||
self.smiley = SmileyWindow(self)
|
|
||||||
self.smiley.setGeometry(QtCore.QRect(self.x() if Settings.get_instance()['mirror_mode'] else 270 + self.x(),
|
|
||||||
self.y() + self.height() - 200,
|
|
||||||
self.smiley.width(),
|
|
||||||
self.smiley.height()))
|
|
||||||
self.smiley.show()
|
|
||||||
|
|
||||||
def send_sticker(self):
|
|
||||||
self.menu.hide()
|
|
||||||
if self.profile.active_friend + 1 and self.profile.is_active_a_friend():
|
|
||||||
self.sticker = StickerWindow(self)
|
|
||||||
self.sticker.setGeometry(QtCore.QRect(self.x() if Settings.get_instance()['mirror_mode'] else 270 + self.x(),
|
|
||||||
self.y() + self.height() - 200,
|
|
||||||
self.sticker.width(),
|
|
||||||
self.sticker.height()))
|
|
||||||
self.sticker.show()
|
|
||||||
|
|
||||||
def active_call(self):
|
|
||||||
self.update_call_state('finish_call')
|
|
||||||
|
|
||||||
def incoming_call(self):
|
|
||||||
self.update_call_state('incoming_call')
|
|
||||||
|
|
||||||
def call_finished(self):
|
|
||||||
self.update_call_state('call')
|
|
||||||
|
|
||||||
def update_call_state(self, state):
|
|
||||||
os.chdir(curr_directory() + '/images/')
|
|
||||||
|
|
||||||
pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(state))
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.callButton.setIcon(icon)
|
|
||||||
self.callButton.setIconSize(QtCore.QSize(50, 50))
|
|
||||||
|
|
||||||
pixmap = QtGui.QPixmap(curr_directory() + '/images/{}_video.png'.format(state))
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.videocallButton.setIcon(icon)
|
|
||||||
self.videocallButton.setIconSize(QtCore.QSize(35, 35))
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Functions which called when user open context menu in friends list
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
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(num)
|
|
||||||
if friend is None:
|
|
||||||
return
|
|
||||||
settings = Settings.get_instance()
|
|
||||||
allowed = friend.tox_id in settings['auto_accept_from_friends']
|
|
||||||
auto = QtWidgets.QApplication.translate("MainWindow", 'Disallow auto accept') if allowed else QtWidgets.QApplication.translate("MainWindow", 'Allow auto accept')
|
|
||||||
if item is not None:
|
|
||||||
self.listMenu = QtWidgets.QMenu()
|
|
||||||
is_friend = type(friend) is Friend
|
|
||||||
if is_friend:
|
|
||||||
set_alias_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Set alias'))
|
|
||||||
set_alias_item.triggered.connect(lambda: self.set_alias(num))
|
|
||||||
|
|
||||||
history_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Chat history'))
|
|
||||||
clear_history_item = history_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Clear history'))
|
|
||||||
export_to_text_item = history_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Export as text'))
|
|
||||||
export_to_html_item = history_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Export as HTML'))
|
|
||||||
|
|
||||||
copy_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Copy'))
|
|
||||||
copy_name_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Name'))
|
|
||||||
copy_status_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Status message'))
|
|
||||||
if is_friend:
|
|
||||||
copy_key_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Public key'))
|
|
||||||
|
|
||||||
auto_accept_item = self.listMenu.addAction(auto)
|
|
||||||
remove_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Remove friend'))
|
|
||||||
block_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Block friend'))
|
|
||||||
notes_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Notes'))
|
|
||||||
|
|
||||||
chats = self.profile.get_group_chats()
|
|
||||||
if len(chats) and self.profile.is_active_online():
|
|
||||||
invite_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Invite to group chat'))
|
|
||||||
for i in range(len(chats)):
|
|
||||||
name, number = chats[i]
|
|
||||||
item = invite_menu.addAction(name)
|
|
||||||
item.triggered.connect(lambda number=number: self.invite_friend_to_gc(num, number))
|
|
||||||
|
|
||||||
plugins_loader = plugin_support.PluginLoader.get_instance()
|
|
||||||
if plugins_loader is not None:
|
|
||||||
submenu = plugins_loader.get_menu(self.listMenu, num)
|
|
||||||
if len(submenu):
|
|
||||||
plug = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Plugins'))
|
|
||||||
plug.addActions(submenu)
|
|
||||||
copy_key_item.triggered.connect(lambda: self.copy_friend_key(num))
|
|
||||||
remove_item.triggered.connect(lambda: self.remove_friend(num))
|
|
||||||
block_item.triggered.connect(lambda: self.block_friend(num))
|
|
||||||
auto_accept_item.triggered.connect(lambda: self.auto_accept(num, not allowed))
|
|
||||||
notes_item.triggered.connect(lambda: self.show_note(friend))
|
|
||||||
else:
|
|
||||||
leave_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Leave chat'))
|
|
||||||
set_title_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Set title'))
|
|
||||||
leave_item.triggered.connect(lambda: self.leave_gc(num))
|
|
||||||
set_title_item.triggered.connect(lambda: self.set_title(num))
|
|
||||||
clear_history_item.triggered.connect(lambda: self.clear_history(num))
|
|
||||||
copy_name_item.triggered.connect(lambda: self.copy_name(friend))
|
|
||||||
copy_status_item.triggered.connect(lambda: self.copy_status(friend))
|
|
||||||
export_to_text_item.triggered.connect(lambda: self.export_history(num))
|
|
||||||
export_to_html_item.triggered.connect(lambda: self.export_history(num, False))
|
|
||||||
parent_position = self.friends_list.mapToGlobal(QtCore.QPoint(0, 0))
|
|
||||||
self.listMenu.move(parent_position + pos)
|
|
||||||
self.listMenu.show()
|
|
||||||
|
|
||||||
def show_note(self, friend):
|
|
||||||
s = Settings.get_instance()
|
|
||||||
note = s['notes'][friend.tox_id] if friend.tox_id in s['notes'] else ''
|
|
||||||
user = QtWidgets.QApplication.translate("MainWindow", 'Notes about user')
|
|
||||||
user = '{} {}'.format(user, friend.name)
|
|
||||||
|
|
||||||
def save_note(text):
|
|
||||||
if friend.tox_id in s['notes']:
|
|
||||||
del s['notes'][friend.tox_id]
|
|
||||||
if text:
|
|
||||||
s['notes'][friend.tox_id] = text
|
|
||||||
s.save()
|
|
||||||
self.note = MultilineEdit(user, note, save_note)
|
|
||||||
self.note.show()
|
|
||||||
|
|
||||||
def export_history(self, num, as_text=True):
|
|
||||||
s = self.profile.export_history(num, as_text)
|
|
||||||
extension = 'txt' if as_text else 'html'
|
|
||||||
file_name, _ = QtWidgets.QFileDialog.getSaveFileName(None,
|
|
||||||
QtWidgets.QApplication.translate("MainWindow",
|
|
||||||
'Choose file name'),
|
|
||||||
curr_directory(),
|
|
||||||
filter=extension,
|
|
||||||
options=QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
|
|
||||||
|
|
||||||
if file_name:
|
|
||||||
if not file_name.endswith('.' + extension):
|
|
||||||
file_name += '.' + extension
|
|
||||||
with open(file_name, 'wt') as fl:
|
|
||||||
fl.write(s)
|
|
||||||
|
|
||||||
def set_alias(self, num):
|
|
||||||
self.profile.set_alias(num)
|
|
||||||
|
|
||||||
def remove_friend(self, num):
|
|
||||||
self.profile.delete_friend(num)
|
|
||||||
|
|
||||||
def block_friend(self, num):
|
|
||||||
friend = self.profile.get_friend(num)
|
|
||||||
self.profile.block_user(friend.tox_id)
|
|
||||||
|
|
||||||
def copy_friend_key(self, num):
|
|
||||||
tox_id = self.profile.friend_public_key(num)
|
|
||||||
clipboard = QtWidgets.QApplication.clipboard()
|
|
||||||
clipboard.setText(tox_id)
|
|
||||||
|
|
||||||
def copy_name(self, friend):
|
|
||||||
clipboard = QtWidgets.QApplication.clipboard()
|
|
||||||
clipboard.setText(friend.name)
|
|
||||||
|
|
||||||
def copy_status(self, friend):
|
|
||||||
clipboard = QtWidgets.QApplication.clipboard()
|
|
||||||
clipboard.setText(friend.status_message)
|
|
||||||
|
|
||||||
def clear_history(self, num):
|
|
||||||
self.profile.clear_history(num)
|
|
||||||
|
|
||||||
def leave_gc(self, num):
|
|
||||||
self.profile.leave_gc(num)
|
|
||||||
|
|
||||||
def set_title(self, num):
|
|
||||||
self.profile.set_title(num)
|
|
||||||
|
|
||||||
def auto_accept(self, num, value):
|
|
||||||
settings = Settings.get_instance()
|
|
||||||
tox_id = self.profile.friend_public_key(num)
|
|
||||||
if value:
|
|
||||||
settings['auto_accept_from_friends'].append(tox_id)
|
|
||||||
else:
|
|
||||||
settings['auto_accept_from_friends'].remove(tox_id)
|
|
||||||
settings.save()
|
|
||||||
|
|
||||||
def invite_friend_to_gc(self, friend_number, group_number):
|
|
||||||
self.profile.invite_friend(friend_number, group_number)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Functions which called when user click somewhere else
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def friend_click(self, index):
|
|
||||||
num = index.row()
|
|
||||||
self.profile.set_active(num)
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
|
||||||
pos = self.connection_status.pos()
|
|
||||||
x, y = pos.x() + self.user_info.pos().x(), pos.y() + self.user_info.pos().y()
|
|
||||||
if (x < event.x() < x + 32) and (y < event.y() < y + 32):
|
|
||||||
self.profile.change_status()
|
|
||||||
else:
|
|
||||||
super(MainWindow, self).mouseReleaseEvent(event)
|
|
||||||
|
|
||||||
def show(self):
|
|
||||||
super().show()
|
|
||||||
self.profile.update()
|
|
||||||
|
|
||||||
def filtering(self):
|
|
||||||
ind = self.online_contacts.currentIndex()
|
|
||||||
d = {0: 0, 1: 1, 2: 2, 3: 4, 4: 1 | 4, 5: 2 | 4}
|
|
||||||
self.profile.filtration_and_sorting(d[ind], self.contact_name.text())
|
|
||||||
|
|
||||||
def show_search_field(self):
|
|
||||||
if hasattr(self, 'search_field') and self.search_field.isVisible():
|
|
||||||
return
|
|
||||||
if self.profile.get_curr_friend() is None:
|
|
||||||
return
|
|
||||||
self.search_field = SearchScreen(self.messages, self.messages.width(), self.messages.parent())
|
|
||||||
x, y = self.messages.x(), self.messages.y() + self.messages.height() - 40
|
|
||||||
self.search_field.setGeometry(x, y, self.messages.width(), 40)
|
|
||||||
self.messages.setGeometry(x, self.messages.y(), self.messages.width(), self.messages.height() - 40)
|
|
||||||
self.search_field.show()
|
|
|
@ -1,492 +0,0 @@
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
||||||
from widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWidget, LineEdit
|
|
||||||
from profile import Profile
|
|
||||||
import smileys
|
|
||||||
import util
|
|
||||||
import platform
|
|
||||||
|
|
||||||
|
|
||||||
class MessageArea(QtWidgets.QPlainTextEdit):
|
|
||||||
"""User types messages here"""
|
|
||||||
|
|
||||||
def __init__(self, parent, form):
|
|
||||||
super(MessageArea, self).__init__(parent)
|
|
||||||
self.parent = form
|
|
||||||
self.setAcceptDrops(True)
|
|
||||||
self.timer = QtCore.QTimer(self)
|
|
||||||
self.timer.timeout.connect(lambda: self.parent.profile.send_typing(False))
|
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
|
||||||
if event.matches(QtGui.QKeySequence.Paste):
|
|
||||||
mimeData = QtWidgets.QApplication.clipboard().mimeData()
|
|
||||||
if mimeData.hasUrls():
|
|
||||||
for url in mimeData.urls():
|
|
||||||
self.pasteEvent(url.toString())
|
|
||||||
else:
|
|
||||||
self.pasteEvent()
|
|
||||||
elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
|
|
||||||
modifiers = event.modifiers()
|
|
||||||
if modifiers & QtCore.Qt.ControlModifier or modifiers & QtCore.Qt.ShiftModifier:
|
|
||||||
self.insertPlainText('\n')
|
|
||||||
else:
|
|
||||||
if self.timer.isActive():
|
|
||||||
self.timer.stop()
|
|
||||||
self.parent.profile.send_typing(False)
|
|
||||||
self.parent.send_message()
|
|
||||||
elif event.key() == QtCore.Qt.Key_Up and not self.toPlainText():
|
|
||||||
self.appendPlainText(Profile.get_instance().get_last_message())
|
|
||||||
elif event.key() == QtCore.Qt.Key_Tab and not self.parent.profile.is_active_a_friend():
|
|
||||||
text = self.toPlainText()
|
|
||||||
pos = self.textCursor().position()
|
|
||||||
self.insertPlainText(Profile.get_instance().get_gc_peer_name(text[:pos]))
|
|
||||||
else:
|
|
||||||
self.parent.profile.send_typing(True)
|
|
||||||
if self.timer.isActive():
|
|
||||||
self.timer.stop()
|
|
||||||
self.timer.start(5000)
|
|
||||||
super(MessageArea, self).keyPressEvent(event)
|
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
|
||||||
menu = create_menu(self.createStandardContextMenu())
|
|
||||||
menu.exec_(event.globalPos())
|
|
||||||
del menu
|
|
||||||
|
|
||||||
def dragEnterEvent(self, e):
|
|
||||||
e.accept()
|
|
||||||
|
|
||||||
def dragMoveEvent(self, e):
|
|
||||||
e.accept()
|
|
||||||
|
|
||||||
def dropEvent(self, e):
|
|
||||||
if e.mimeData().hasFormat('text/plain') or e.mimeData().hasFormat('text/html'):
|
|
||||||
e.accept()
|
|
||||||
self.pasteEvent(e.mimeData().text())
|
|
||||||
elif e.mimeData().hasUrls():
|
|
||||||
for url in e.mimeData().urls():
|
|
||||||
self.pasteEvent(url.toString())
|
|
||||||
e.accept()
|
|
||||||
else:
|
|
||||||
e.ignore()
|
|
||||||
|
|
||||||
def pasteEvent(self, text=None):
|
|
||||||
text = text or QtWidgets.QApplication.clipboard().text()
|
|
||||||
if text.startswith('file://'):
|
|
||||||
file_name = self.parse_file_name(text)
|
|
||||||
self.parent.profile.send_file(file_name)
|
|
||||||
elif text:
|
|
||||||
self.insertPlainText(text)
|
|
||||||
else:
|
|
||||||
image = QtWidgets.QApplication.clipboard().image()
|
|
||||||
if image is not None:
|
|
||||||
byte_array = QtCore.QByteArray()
|
|
||||||
buffer = QtCore.QBuffer(byte_array)
|
|
||||||
buffer.open(QtCore.QIODevice.WriteOnly)
|
|
||||||
image.save(buffer, 'PNG')
|
|
||||||
self.parent.profile.send_screenshot(bytes(byte_array.data()))
|
|
||||||
|
|
||||||
def parse_file_name(self, file_name):
|
|
||||||
import urllib
|
|
||||||
if file_name.endswith('\r\n'):
|
|
||||||
file_name = file_name[:-2]
|
|
||||||
file_name = urllib.parse.unquote(file_name)
|
|
||||||
return file_name[8 if platform.system() == 'Windows' else 7:]
|
|
||||||
|
|
||||||
|
|
||||||
class ScreenShotWindow(RubberBandWindow):
|
|
||||||
|
|
||||||
def closeEvent(self, *args):
|
|
||||||
if self.parent.isHidden():
|
|
||||||
self.parent.show()
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
|
||||||
if self.rubberband.isVisible():
|
|
||||||
self.rubberband.hide()
|
|
||||||
rect = self.rubberband.geometry()
|
|
||||||
if rect.width() and rect.height():
|
|
||||||
screen = QtWidgets.QApplication.primaryScreen()
|
|
||||||
p = screen.grabWindow(0,
|
|
||||||
rect.x() + 4,
|
|
||||||
rect.y() + 4,
|
|
||||||
rect.width() - 8,
|
|
||||||
rect.height() - 8)
|
|
||||||
byte_array = QtCore.QByteArray()
|
|
||||||
buffer = QtCore.QBuffer(byte_array)
|
|
||||||
buffer.open(QtCore.QIODevice.WriteOnly)
|
|
||||||
p.save(buffer, 'PNG')
|
|
||||||
Profile.get_instance().send_screenshot(bytes(byte_array.data()))
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
class SmileyWindow(QtWidgets.QWidget):
|
|
||||||
"""
|
|
||||||
Smiley selection window
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
super(SmileyWindow, self).__init__()
|
|
||||||
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
|
|
||||||
inst = smileys.SmileyLoader.get_instance()
|
|
||||||
self.data = inst.get_smileys()
|
|
||||||
count = len(self.data)
|
|
||||||
if not count:
|
|
||||||
self.close()
|
|
||||||
self.page_size = int(pow(count / 8, 0.5) + 1) * 8 # smileys per page
|
|
||||||
if count % self.page_size == 0:
|
|
||||||
self.page_count = count // self.page_size
|
|
||||||
else:
|
|
||||||
self.page_count = round(count / self.page_size + 0.5)
|
|
||||||
self.page = -1
|
|
||||||
self.radio = []
|
|
||||||
self.parent = parent
|
|
||||||
for i in range(self.page_count): # buttons with smileys
|
|
||||||
elem = QtWidgets.QRadioButton(self)
|
|
||||||
elem.setGeometry(QtCore.QRect(i * 20 + 5, 180, 20, 20))
|
|
||||||
elem.clicked.connect(lambda c, t=i: self.checked(t))
|
|
||||||
self.radio.append(elem)
|
|
||||||
width = max(self.page_count * 20 + 30, (self.page_size + 5) * 8 // 10)
|
|
||||||
self.setMaximumSize(width, 200)
|
|
||||||
self.setMinimumSize(width, 200)
|
|
||||||
self.buttons = []
|
|
||||||
for i in range(self.page_size): # pages - radio buttons
|
|
||||||
b = QtWidgets.QPushButton(self)
|
|
||||||
b.setGeometry(QtCore.QRect((i // 8) * 20 + 5, (i % 8) * 20, 20, 20))
|
|
||||||
b.clicked.connect(lambda c, t=i: self.clicked(t))
|
|
||||||
self.buttons.append(b)
|
|
||||||
self.checked(0)
|
|
||||||
|
|
||||||
def checked(self, pos): # new page opened
|
|
||||||
self.radio[self.page].setChecked(False)
|
|
||||||
self.radio[pos].setChecked(True)
|
|
||||||
self.page = pos
|
|
||||||
start = self.page * self.page_size
|
|
||||||
for i in range(self.page_size):
|
|
||||||
try:
|
|
||||||
self.buttons[i].setVisible(True)
|
|
||||||
pixmap = QtGui.QPixmap(self.data[start + i][1])
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.buttons[i].setIcon(icon)
|
|
||||||
except:
|
|
||||||
self.buttons[i].setVisible(False)
|
|
||||||
|
|
||||||
def clicked(self, pos): # smiley selected
|
|
||||||
pos += self.page * self.page_size
|
|
||||||
smiley = self.data[pos][0]
|
|
||||||
self.parent.messageEdit.insertPlainText(smiley)
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def leaveEvent(self, event):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
class MenuButton(QtWidgets.QPushButton):
|
|
||||||
|
|
||||||
def __init__(self, parent, enter):
|
|
||||||
super(MenuButton, self).__init__(parent)
|
|
||||||
self.enter = enter
|
|
||||||
|
|
||||||
def enterEvent(self, event):
|
|
||||||
self.enter()
|
|
||||||
super(MenuButton, self).enterEvent(event)
|
|
||||||
|
|
||||||
|
|
||||||
class DropdownMenu(QtWidgets.QWidget):
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
super(DropdownMenu, self).__init__(parent)
|
|
||||||
self.installEventFilter(self)
|
|
||||||
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
|
|
||||||
self.setMaximumSize(120, 120)
|
|
||||||
self.setMinimumSize(120, 120)
|
|
||||||
self.screenshotButton = QRightClickButton(self)
|
|
||||||
self.screenshotButton.setGeometry(QtCore.QRect(0, 60, 60, 60))
|
|
||||||
self.screenshotButton.setObjectName("screenshotButton")
|
|
||||||
|
|
||||||
self.fileTransferButton = QtWidgets.QPushButton(self)
|
|
||||||
self.fileTransferButton.setGeometry(QtCore.QRect(60, 60, 60, 60))
|
|
||||||
self.fileTransferButton.setObjectName("fileTransferButton")
|
|
||||||
|
|
||||||
self.smileyButton = QtWidgets.QPushButton(self)
|
|
||||||
self.smileyButton.setGeometry(QtCore.QRect(0, 0, 60, 60))
|
|
||||||
|
|
||||||
self.stickerButton = QtWidgets.QPushButton(self)
|
|
||||||
self.stickerButton.setGeometry(QtCore.QRect(60, 0, 60, 60))
|
|
||||||
|
|
||||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/file.png')
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.fileTransferButton.setIcon(icon)
|
|
||||||
self.fileTransferButton.setIconSize(QtCore.QSize(50, 50))
|
|
||||||
|
|
||||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/screenshot.png')
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.screenshotButton.setIcon(icon)
|
|
||||||
self.screenshotButton.setIconSize(QtCore.QSize(50, 60))
|
|
||||||
|
|
||||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/smiley.png')
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.smileyButton.setIcon(icon)
|
|
||||||
self.smileyButton.setIconSize(QtCore.QSize(50, 50))
|
|
||||||
|
|
||||||
pixmap = QtGui.QPixmap(util.curr_directory() + '/images/sticker.png')
|
|
||||||
icon = QtGui.QIcon(pixmap)
|
|
||||||
self.stickerButton.setIcon(icon)
|
|
||||||
self.stickerButton.setIconSize(QtCore.QSize(55, 55))
|
|
||||||
|
|
||||||
self.screenshotButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Send screenshot"))
|
|
||||||
self.fileTransferButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Send file"))
|
|
||||||
self.smileyButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Add smiley"))
|
|
||||||
self.stickerButton.setToolTip(QtWidgets.QApplication.translate("MenuWindow", "Send sticker"))
|
|
||||||
|
|
||||||
self.fileTransferButton.clicked.connect(parent.send_file)
|
|
||||||
self.screenshotButton.clicked.connect(parent.send_screenshot)
|
|
||||||
self.screenshotButton.rightClicked.connect(lambda: parent.send_screenshot(True))
|
|
||||||
self.smileyButton.clicked.connect(parent.send_smiley)
|
|
||||||
self.stickerButton.clicked.connect(parent.send_sticker)
|
|
||||||
|
|
||||||
def leaveEvent(self, event):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def eventFilter(self, obj, event):
|
|
||||||
if event.type() == QtCore.QEvent.WindowDeactivate:
|
|
||||||
self.close()
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class StickerItem(QtWidgets.QWidget):
|
|
||||||
|
|
||||||
def __init__(self, fl):
|
|
||||||
super(StickerItem, self).__init__()
|
|
||||||
self._image_label = QtWidgets.QLabel(self)
|
|
||||||
self.path = fl
|
|
||||||
self.pixmap = QtGui.QPixmap()
|
|
||||||
self.pixmap.load(fl)
|
|
||||||
if self.pixmap.width() > 150:
|
|
||||||
self.pixmap = self.pixmap.scaled(150, 200, QtCore.Qt.KeepAspectRatio)
|
|
||||||
self.setFixedSize(150, self.pixmap.height())
|
|
||||||
self._image_label.setPixmap(self.pixmap)
|
|
||||||
|
|
||||||
|
|
||||||
class StickerWindow(QtWidgets.QWidget):
|
|
||||||
"""Sticker selection window"""
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
super(StickerWindow, self).__init__()
|
|
||||||
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
|
|
||||||
self.setMaximumSize(250, 200)
|
|
||||||
self.setMinimumSize(250, 200)
|
|
||||||
self.list = QtWidgets.QListWidget(self)
|
|
||||||
self.list.setGeometry(QtCore.QRect(0, 0, 250, 200))
|
|
||||||
self.arr = smileys.sticker_loader()
|
|
||||||
for sticker in self.arr:
|
|
||||||
item = StickerItem(sticker)
|
|
||||||
elem = QtWidgets.QListWidgetItem()
|
|
||||||
elem.setSizeHint(QtCore.QSize(250, item.height()))
|
|
||||||
self.list.addItem(elem)
|
|
||||||
self.list.setItemWidget(elem, item)
|
|
||||||
self.list.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
|
||||||
self.list.setSpacing(3)
|
|
||||||
self.list.clicked.connect(self.click)
|
|
||||||
self.parent = parent
|
|
||||||
|
|
||||||
def click(self, index):
|
|
||||||
num = index.row()
|
|
||||||
self.parent.profile.send_sticker(self.arr[num])
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def leaveEvent(self, event):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
class WelcomeScreen(CenteredWidget):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.setMaximumSize(250, 200)
|
|
||||||
self.setMinimumSize(250, 200)
|
|
||||||
self.center()
|
|
||||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
|
||||||
self.text = QtWidgets.QTextBrowser(self)
|
|
||||||
self.text.setGeometry(QtCore.QRect(0, 0, 250, 170))
|
|
||||||
self.text.setOpenExternalLinks(True)
|
|
||||||
self.checkbox = QtWidgets.QCheckBox(self)
|
|
||||||
self.checkbox.setGeometry(QtCore.QRect(5, 170, 240, 30))
|
|
||||||
self.checkbox.setText(QtWidgets.QApplication.translate('WelcomeScreen', "Don't show again"))
|
|
||||||
self.setWindowTitle(QtWidgets.QApplication.translate('WelcomeScreen', 'Tip of the day'))
|
|
||||||
import random
|
|
||||||
num = random.randint(0, 10)
|
|
||||||
if num == 0:
|
|
||||||
text = QtWidgets.QApplication.translate('WelcomeScreen', 'Press Esc if you want hide app to tray.')
|
|
||||||
elif num == 1:
|
|
||||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
|
||||||
'Right click on screenshot button hides app to tray during screenshot.')
|
|
||||||
elif num == 2:
|
|
||||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
|
||||||
'You can use Tox over Tor. For more info read <a href="https://wiki.tox.chat/users/tox_over_tor_tot">this post</a>')
|
|
||||||
elif num == 3:
|
|
||||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
|
||||||
'Use Settings -> Interface to customize interface.')
|
|
||||||
elif num == 4:
|
|
||||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
|
||||||
'Set profile password via Profile -> Settings. Password allows Toxygen encrypt your history and settings.')
|
|
||||||
elif num == 5:
|
|
||||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
|
||||||
'Since v0.1.3 Toxygen supports plugins. <a href="https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md">Read more</a>')
|
|
||||||
elif num == 6:
|
|
||||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
|
||||||
'Toxygen supports faux offline messages and file transfers. Send message or file to offline friend and he will get it later.')
|
|
||||||
elif num == 7:
|
|
||||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
|
||||||
'New in Toxygen 0.4.1:<br>Downloading nodes from tox.chat<br>Bug fixes')
|
|
||||||
elif num == 8:
|
|
||||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
|
||||||
'Delete single message in chat: make right click on spinner or message time and choose "Delete" in menu')
|
|
||||||
elif num == 9:
|
|
||||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
|
||||||
'Use right click on inline image to save it')
|
|
||||||
else:
|
|
||||||
text = QtWidgets.QApplication.translate('WelcomeScreen',
|
|
||||||
'Set new NoSpam to avoid spam friend requests: Profile -> Settings -> Set new NoSpam.')
|
|
||||||
self.text.setHtml(text)
|
|
||||||
self.checkbox.stateChanged.connect(self.not_show)
|
|
||||||
QtCore.QTimer.singleShot(1000, self.show)
|
|
||||||
|
|
||||||
def not_show(self):
|
|
||||||
import settings
|
|
||||||
s = settings.Settings.get_instance()
|
|
||||||
s['show_welcome_screen'] = False
|
|
||||||
s.save()
|
|
||||||
|
|
||||||
|
|
||||||
class MainMenuButton(QtWidgets.QPushButton):
|
|
||||||
|
|
||||||
def __init__(self, *args):
|
|
||||||
super().__init__(*args)
|
|
||||||
self.setObjectName("mainmenubutton")
|
|
||||||
|
|
||||||
def setText(self, text):
|
|
||||||
metrics = QtGui.QFontMetrics(self.font())
|
|
||||||
self.setFixedWidth(metrics.size(QtCore.Qt.TextSingleLine, text).width() + 20)
|
|
||||||
super().setText(text)
|
|
||||||
|
|
||||||
|
|
||||||
class ClickableLabel(QtWidgets.QLabel):
|
|
||||||
|
|
||||||
clicked = QtCore.pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, *args):
|
|
||||||
super().__init__(*args)
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
|
||||||
self.clicked.emit()
|
|
||||||
|
|
||||||
|
|
||||||
class SearchScreen(QtWidgets.QWidget):
|
|
||||||
|
|
||||||
def __init__(self, messages, width, *args):
|
|
||||||
super().__init__(*args)
|
|
||||||
self.setMaximumSize(width, 40)
|
|
||||||
self.setMinimumSize(width, 40)
|
|
||||||
self._messages = messages
|
|
||||||
|
|
||||||
self.search_text = LineEdit(self)
|
|
||||||
self.search_text.setGeometry(0, 0, width - 160, 40)
|
|
||||||
|
|
||||||
self.search_button = ClickableLabel(self)
|
|
||||||
self.search_button.setGeometry(width - 160, 0, 40, 40)
|
|
||||||
pixmap = QtGui.QPixmap()
|
|
||||||
pixmap.load(util.curr_directory() + '/images/search.png')
|
|
||||||
self.search_button.setScaledContents(False)
|
|
||||||
self.search_button.setAlignment(QtCore.Qt.AlignCenter)
|
|
||||||
self.search_button.setPixmap(pixmap)
|
|
||||||
self.search_button.clicked.connect(self.search)
|
|
||||||
|
|
||||||
font = QtGui.QFont()
|
|
||||||
font.setPointSize(32)
|
|
||||||
font.setBold(True)
|
|
||||||
|
|
||||||
self.prev_button = QtWidgets.QPushButton(self)
|
|
||||||
self.prev_button.setGeometry(width - 120, 0, 40, 40)
|
|
||||||
self.prev_button.clicked.connect(self.prev)
|
|
||||||
self.prev_button.setText('\u25B2')
|
|
||||||
|
|
||||||
self.next_button = QtWidgets.QPushButton(self)
|
|
||||||
self.next_button.setGeometry(width - 80, 0, 40, 40)
|
|
||||||
self.next_button.clicked.connect(self.next)
|
|
||||||
self.next_button.setText('\u25BC')
|
|
||||||
|
|
||||||
self.close_button = QtWidgets.QPushButton(self)
|
|
||||||
self.close_button.setGeometry(width - 40, 0, 40, 40)
|
|
||||||
self.close_button.clicked.connect(self.close)
|
|
||||||
self.close_button.setText('×')
|
|
||||||
self.close_button.setFont(font)
|
|
||||||
|
|
||||||
font.setPointSize(18)
|
|
||||||
self.next_button.setFont(font)
|
|
||||||
self.prev_button.setFont(font)
|
|
||||||
|
|
||||||
self.retranslateUi()
|
|
||||||
|
|
||||||
def retranslateUi(self):
|
|
||||||
self.search_text.setPlaceholderText(QtWidgets.QApplication.translate("MainWindow", "Search"))
|
|
||||||
|
|
||||||
def show(self):
|
|
||||||
super().show()
|
|
||||||
self.search_text.setFocus()
|
|
||||||
|
|
||||||
def search(self):
|
|
||||||
Profile.get_instance().update()
|
|
||||||
text = self.search_text.text()
|
|
||||||
friend = Profile.get_instance().get_curr_friend()
|
|
||||||
if text and friend and util.is_re_valid(text):
|
|
||||||
index = friend.search_string(text)
|
|
||||||
self.load_messages(index)
|
|
||||||
|
|
||||||
def prev(self):
|
|
||||||
friend = Profile.get_instance().get_curr_friend()
|
|
||||||
if friend is not None:
|
|
||||||
index = friend.search_prev()
|
|
||||||
self.load_messages(index)
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
friend = Profile.get_instance().get_curr_friend()
|
|
||||||
text = self.search_text.text()
|
|
||||||
if friend is not None:
|
|
||||||
index = friend.search_next()
|
|
||||||
if index is not None:
|
|
||||||
count = self._messages.count()
|
|
||||||
index += count
|
|
||||||
item = self._messages.item(index)
|
|
||||||
self._messages.scrollToItem(item)
|
|
||||||
self._messages.itemWidget(item).select_text(text)
|
|
||||||
else:
|
|
||||||
self.not_found(text)
|
|
||||||
|
|
||||||
def load_messages(self, index):
|
|
||||||
text = self.search_text.text()
|
|
||||||
if index is not None:
|
|
||||||
profile = Profile.get_instance()
|
|
||||||
count = self._messages.count()
|
|
||||||
while count + index < 0:
|
|
||||||
profile.load_history()
|
|
||||||
count = self._messages.count()
|
|
||||||
index += count
|
|
||||||
item = self._messages.item(index)
|
|
||||||
self._messages.scrollToItem(item)
|
|
||||||
self._messages.itemWidget(item).select_text(text)
|
|
||||||
else:
|
|
||||||
self.not_found(text)
|
|
||||||
|
|
||||||
def closeEvent(self, *args):
|
|
||||||
Profile.get_instance().update()
|
|
||||||
self._messages.setGeometry(0, 0, self._messages.width(), self._messages.height() + 40)
|
|
||||||
super().closeEvent(*args)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def not_found(text):
|
|
||||||
mbox = QtWidgets.QMessageBox()
|
|
||||||
mbox_text = QtWidgets.QApplication.translate("MainWindow",
|
|
||||||
'Text "{}" was not found')
|
|
||||||
|
|
||||||
mbox.setText(mbox_text.format(text))
|
|
||||||
mbox.setWindowTitle(QtWidgets.QApplication.translate("MainWindow",
|
|
||||||
'Not found'))
|
|
||||||
mbox.exec_()
|
|
1095
toxygen/menu.py
1095
toxygen/menu.py
File diff suppressed because it is too large
Load diff
|
@ -1,113 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
MESSAGE_TYPE = {
|
|
||||||
'TEXT': 0,
|
|
||||||
'ACTION': 1,
|
|
||||||
'FILE_TRANSFER': 2,
|
|
||||||
'INLINE': 3,
|
|
||||||
'INFO_MESSAGE': 4,
|
|
||||||
'GC_TEXT': 5,
|
|
||||||
'GC_ACTION': 6
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Message:
|
|
||||||
|
|
||||||
def __init__(self, message_type, owner, time):
|
|
||||||
self._time = time
|
|
||||||
self._type = message_type
|
|
||||||
self._owner = owner
|
|
||||||
|
|
||||||
def get_type(self):
|
|
||||||
return self._type
|
|
||||||
|
|
||||||
def get_owner(self):
|
|
||||||
return self._owner
|
|
||||||
|
|
||||||
def mark_as_sent(self):
|
|
||||||
self._owner = 0
|
|
||||||
|
|
||||||
|
|
||||||
class TextMessage(Message):
|
|
||||||
"""
|
|
||||||
Plain text or action message
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message, owner, time, message_type):
|
|
||||||
super(TextMessage, self).__init__(message_type, owner, time)
|
|
||||||
self._message = message
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return self._message, self._owner, self._time, self._type
|
|
||||||
|
|
||||||
|
|
||||||
class GroupChatMessage(TextMessage):
|
|
||||||
|
|
||||||
def __init__(self, message, owner, time, message_type, name):
|
|
||||||
super().__init__(message, owner, time, message_type)
|
|
||||||
self._user_name = name
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return self._message, self._owner, self._time, self._type, self._user_name
|
|
||||||
|
|
||||||
|
|
||||||
class TransferMessage(Message):
|
|
||||||
"""
|
|
||||||
Message with info about file transfer
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, owner, time, status, size, name, friend_number, file_number):
|
|
||||||
super(TransferMessage, self).__init__(MESSAGE_TYPE['FILE_TRANSFER'], owner, time)
|
|
||||||
self._status = status
|
|
||||||
self._size = size
|
|
||||||
self._file_name = name
|
|
||||||
self._friend_number, self._file_number = friend_number, file_number
|
|
||||||
|
|
||||||
def is_active(self, file_number):
|
|
||||||
return self._file_number == file_number and self._status not in (2, 3)
|
|
||||||
|
|
||||||
def get_friend_number(self):
|
|
||||||
return self._friend_number
|
|
||||||
|
|
||||||
def get_file_number(self):
|
|
||||||
return self._file_number
|
|
||||||
|
|
||||||
def get_status(self):
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
def set_status(self, value):
|
|
||||||
self._status = value
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return self._file_name, self._size, self._time, self._owner, self._friend_number, self._file_number, self._status
|
|
||||||
|
|
||||||
|
|
||||||
class UnsentFile(Message):
|
|
||||||
def __init__(self, path, data, time):
|
|
||||||
super(UnsentFile, self).__init__(MESSAGE_TYPE['FILE_TRANSFER'], 0, time)
|
|
||||||
self._data, self._path = data, path
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return self._path, self._data, self._time
|
|
||||||
|
|
||||||
def get_status(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class InlineImage(Message):
|
|
||||||
"""
|
|
||||||
Inline image
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
super(InlineImage, self).__init__(MESSAGE_TYPE['INLINE'], None, None)
|
|
||||||
self._data = data
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return self._data
|
|
||||||
|
|
||||||
|
|
||||||
class InfoMessage(TextMessage):
|
|
||||||
|
|
||||||
def __init__(self, message, time):
|
|
||||||
super(InfoMessage, self).__init__(message, None, time, MESSAGE_TYPE['INFO_MESSAGE'])
|
|
File diff suppressed because one or more lines are too long
|
@ -1,71 +0,0 @@
|
||||||
from PyQt5 import QtCore, QtWidgets
|
|
||||||
from util import curr_directory
|
|
||||||
import wave
|
|
||||||
import pyaudio
|
|
||||||
|
|
||||||
|
|
||||||
SOUND_NOTIFICATION = {
|
|
||||||
'MESSAGE': 0,
|
|
||||||
'FRIEND_CONNECTION_STATUS': 1,
|
|
||||||
'FILE_TRANSFER': 2
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def tray_notification(title, text, tray, window):
|
|
||||||
"""
|
|
||||||
Show tray notification and activate window icon
|
|
||||||
NOTE: different behaviour on different OS
|
|
||||||
:param title: Name of user who sent message or file
|
|
||||||
:param text: text of message or file info
|
|
||||||
:param tray: ref to tray icon
|
|
||||||
:param window: main window
|
|
||||||
"""
|
|
||||||
if QtWidgets.QSystemTrayIcon.isSystemTrayAvailable():
|
|
||||||
if len(text) > 30:
|
|
||||||
text = text[:27] + '...'
|
|
||||||
tray.showMessage(title, text, QtWidgets.QSystemTrayIcon.NoIcon, 3000)
|
|
||||||
QtWidgets.QApplication.alert(window, 0)
|
|
||||||
|
|
||||||
def message_clicked():
|
|
||||||
window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
|
|
||||||
window.activateWindow()
|
|
||||||
tray.messageClicked.connect(message_clicked)
|
|
||||||
|
|
||||||
|
|
||||||
class AudioFile:
|
|
||||||
chunk = 1024
|
|
||||||
|
|
||||||
def __init__(self, fl):
|
|
||||||
self.wf = wave.open(fl, 'rb')
|
|
||||||
self.p = pyaudio.PyAudio()
|
|
||||||
self.stream = self.p.open(
|
|
||||||
format=self.p.get_format_from_width(self.wf.getsampwidth()),
|
|
||||||
channels=self.wf.getnchannels(),
|
|
||||||
rate=self.wf.getframerate(),
|
|
||||||
output=True)
|
|
||||||
|
|
||||||
def play(self):
|
|
||||||
data = self.wf.readframes(self.chunk)
|
|
||||||
while data:
|
|
||||||
self.stream.write(data)
|
|
||||||
data = self.wf.readframes(self.chunk)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.stream.close()
|
|
||||||
self.p.terminate()
|
|
||||||
|
|
||||||
|
|
||||||
def sound_notification(t):
|
|
||||||
"""
|
|
||||||
Plays sound notification
|
|
||||||
:param t: type of notification
|
|
||||||
"""
|
|
||||||
if t == SOUND_NOTIFICATION['MESSAGE']:
|
|
||||||
f = curr_directory() + '/sounds/message.wav'
|
|
||||||
elif t == SOUND_NOTIFICATION['FILE_TRANSFER']:
|
|
||||||
f = curr_directory() + '/sounds/file.wav'
|
|
||||||
else:
|
|
||||||
f = curr_directory() + '/sounds/contact.wav'
|
|
||||||
a = AudioFile(f)
|
|
||||||
a.play()
|
|
||||||
a.close()
|
|
|
@ -1,154 +0,0 @@
|
||||||
from widgets import CenteredWidget, LineEdit
|
|
||||||
from PyQt5 import QtCore, QtWidgets
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordArea(LineEdit):
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
super(PasswordArea, self).__init__(parent)
|
|
||||||
self.parent = parent
|
|
||||||
self.setEchoMode(QtWidgets.QLineEdit.Password)
|
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
|
||||||
if event.key() == QtCore.Qt.Key_Return:
|
|
||||||
self.parent.button_click()
|
|
||||||
else:
|
|
||||||
super(PasswordArea, self).keyPressEvent(event)
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordScreenBase(CenteredWidget):
|
|
||||||
|
|
||||||
def __init__(self, encrypt):
|
|
||||||
super(PasswordScreenBase, self).__init__()
|
|
||||||
self._encrypt = encrypt
|
|
||||||
self.initUI()
|
|
||||||
|
|
||||||
def initUI(self):
|
|
||||||
self.resize(360, 170)
|
|
||||||
self.setMinimumSize(QtCore.QSize(360, 170))
|
|
||||||
self.setMaximumSize(QtCore.QSize(360, 170))
|
|
||||||
|
|
||||||
self.enter_pass = QtWidgets.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 = QtWidgets.QPushButton(self)
|
|
||||||
self.button.setGeometry(QtCore.QRect(30, 90, 300, 30))
|
|
||||||
self.button.setText('OK')
|
|
||||||
self.button.clicked.connect(self.button_click)
|
|
||||||
|
|
||||||
self.warning = QtWidgets.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):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
|
||||||
if event.key() == QtCore.Qt.Key_Enter:
|
|
||||||
self.button_click()
|
|
||||||
else:
|
|
||||||
super(PasswordScreenBase, self).keyPressEvent(event)
|
|
||||||
|
|
||||||
def retranslateUi(self):
|
|
||||||
self.setWindowTitle(QtWidgets.QApplication.translate("pass", "Enter password"))
|
|
||||||
self.enter_pass.setText(QtWidgets.QApplication.translate("pass", "Password:"))
|
|
||||||
self.warning.setText(QtWidgets.QApplication.translate("pass", "Incorrect password"))
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordScreen(PasswordScreenBase):
|
|
||||||
|
|
||||||
def __init__(self, encrypt, data):
|
|
||||||
super(PasswordScreen, self).__init__(encrypt)
|
|
||||||
self._data = data
|
|
||||||
|
|
||||||
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 as ex:
|
|
||||||
self.warning.setVisible(True)
|
|
||||||
print('Decryption error:', ex)
|
|
||||||
else:
|
|
||||||
self._data[0] = new_data
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
class UnlockAppScreen(PasswordScreenBase):
|
|
||||||
|
|
||||||
def __init__(self, encrypt, callback):
|
|
||||||
super(UnlockAppScreen, self).__init__(encrypt)
|
|
||||||
self._callback = callback
|
|
||||||
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
|
|
||||||
|
|
||||||
def button_click(self):
|
|
||||||
if self.password.text():
|
|
||||||
if self._encrypt.is_password(self.password.text()):
|
|
||||||
self._callback()
|
|
||||||
self.close()
|
|
||||||
else:
|
|
||||||
self.warning.setVisible(True)
|
|
||||||
print('Wrong password!')
|
|
||||||
|
|
||||||
|
|
||||||
class SetProfilePasswordScreen(CenteredWidget):
|
|
||||||
|
|
||||||
def __init__(self, encrypt):
|
|
||||||
super(SetProfilePasswordScreen, self).__init__()
|
|
||||||
self._encrypt = encrypt
|
|
||||||
self.initUI()
|
|
||||||
self.retranslateUi()
|
|
||||||
self.center()
|
|
||||||
|
|
||||||
def initUI(self):
|
|
||||||
self.setMinimumSize(QtCore.QSize(700, 200))
|
|
||||||
self.setMaximumSize(QtCore.QSize(700, 200))
|
|
||||||
self.password = LineEdit(self)
|
|
||||||
self.password.setGeometry(QtCore.QRect(40, 10, 300, 30))
|
|
||||||
self.password.setEchoMode(QtWidgets.QLineEdit.Password)
|
|
||||||
self.confirm_password = LineEdit(self)
|
|
||||||
self.confirm_password.setGeometry(QtCore.QRect(40, 50, 300, 30))
|
|
||||||
self.confirm_password.setEchoMode(QtWidgets.QLineEdit.Password)
|
|
||||||
self.set_password = QtWidgets.QPushButton(self)
|
|
||||||
self.set_password.setGeometry(QtCore.QRect(40, 100, 300, 30))
|
|
||||||
self.set_password.clicked.connect(self.new_password)
|
|
||||||
self.not_match = QtWidgets.QLabel(self)
|
|
||||||
self.not_match.setGeometry(QtCore.QRect(350, 50, 300, 30))
|
|
||||||
self.not_match.setVisible(False)
|
|
||||||
self.not_match.setStyleSheet('QLabel { color: #BC1C1C; }')
|
|
||||||
self.warning = QtWidgets.QLabel(self)
|
|
||||||
self.warning.setGeometry(QtCore.QRect(40, 160, 500, 30))
|
|
||||||
self.warning.setStyleSheet('QLabel { color: #BC1C1C; }')
|
|
||||||
|
|
||||||
def retranslateUi(self):
|
|
||||||
self.setWindowTitle(QtWidgets.QApplication.translate("PasswordScreen", "Profile password"))
|
|
||||||
self.password.setPlaceholderText(
|
|
||||||
QtWidgets.QApplication.translate("PasswordScreen", "Password (at least 8 symbols)"))
|
|
||||||
self.confirm_password.setPlaceholderText(
|
|
||||||
QtWidgets.QApplication.translate("PasswordScreen", "Confirm password"))
|
|
||||||
self.set_password.setText(
|
|
||||||
QtWidgets.QApplication.translate("PasswordScreen", "Set password"))
|
|
||||||
self.not_match.setText(QtWidgets.QApplication.translate("PasswordScreen", "Passwords do not match"))
|
|
||||||
self.warning.setText(
|
|
||||||
QtWidgets.QApplication.translate("PasswordScreen", "There is no way to recover lost passwords"))
|
|
||||||
|
|
||||||
def new_password(self):
|
|
||||||
if self.password.text() == self.confirm_password.text():
|
|
||||||
if len(self.password.text()) >= 8:
|
|
||||||
self._encrypt.set_password(self.password.text())
|
|
||||||
self.close()
|
|
||||||
else:
|
|
||||||
self.not_match.setText(
|
|
||||||
QtWidgets.QApplication.translate("PasswordScreen", "Password must be at least 8 symbols"))
|
|
||||||
self.not_match.setVisible(True)
|
|
||||||
else:
|
|
||||||
self.not_match.setText(QtWidgets.QApplication.translate("PasswordScreen", "Passwords do not match"))
|
|
||||||
self.not_match.setVisible(True)
|
|
|
@ -1,176 +0,0 @@
|
||||||
import util
|
|
||||||
import profile
|
|
||||||
import os
|
|
||||||
import importlib
|
|
||||||
import inspect
|
|
||||||
import plugins.plugin_super_class as pl
|
|
||||||
import toxes
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
class PluginLoader(util.Singleton):
|
|
||||||
|
|
||||||
def __init__(self, tox, settings):
|
|
||||||
super().__init__()
|
|
||||||
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 = toxes.ToxES.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/'
|
|
||||||
if not os.path.exists(path):
|
|
||||||
util.log('Plugin dir not found')
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
sys.path.append(path)
|
|
||||||
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 = importlib.import_module(name) # 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)
|
|
||||||
# looking for plugin class in module
|
|
||||||
if inspect.isclass(obj) and hasattr(obj, 'is_plugin') and obj.is_plugin:
|
|
||||||
print('Plugin', elem)
|
|
||||||
try: # create instance of plugin class
|
|
||||||
inst = obj(self._tox, self._profile, self._settings, self._encr)
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
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:]), friend_number)
|
|
||||||
|
|
||||||
def callback_lossy(self, friend_number, data):
|
|
||||||
"""
|
|
||||||
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:]), friend_number)
|
|
||||||
|
|
||||||
def friend_online(self, friend_number):
|
|
||||||
"""
|
|
||||||
Friend with specified number is online
|
|
||||||
"""
|
|
||||||
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():
|
|
||||||
try:
|
|
||||||
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
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
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 get_message_menu(self, menu, selected_text):
|
|
||||||
result = []
|
|
||||||
for elem in self._plugins.values():
|
|
||||||
if elem[1]:
|
|
||||||
try:
|
|
||||||
result.extend(elem[0].get_message_menu(menu, selected_text))
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
return result
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""
|
|
||||||
App is closing, stop all plugins
|
|
||||||
"""
|
|
||||||
for key in list(self._plugins.keys()):
|
|
||||||
if self._plugins[key][1]:
|
|
||||||
self._plugins[key][0].close()
|
|
||||||
del self._plugins[key]
|
|
||||||
|
|
||||||
def reload(self):
|
|
||||||
print('Reloading plugins')
|
|
||||||
self.stop()
|
|
||||||
self.load()
|
|
1466
toxygen/profile.py
1466
toxygen/profile.py
File diff suppressed because it is too large
Load diff
|
@ -1,22 +0,0 @@
|
||||||
import numpy as np
|
|
||||||
from PyQt5 import QtWidgets
|
|
||||||
|
|
||||||
|
|
||||||
class DesktopGrabber:
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height):
|
|
||||||
self._x = x
|
|
||||||
self._y = y
|
|
||||||
self._width = width
|
|
||||||
self._height = height
|
|
||||||
self._width -= width % 4
|
|
||||||
self._height -= height % 4
|
|
||||||
self._screen = QtWidgets.QApplication.primaryScreen()
|
|
||||||
|
|
||||||
def read(self):
|
|
||||||
pixmap = self._screen.grabWindow(0, self._x, self._y, self._width, self._height)
|
|
||||||
image = pixmap.toImage()
|
|
||||||
s = image.bits().asstring(self._width * self._height * 4)
|
|
||||||
arr = np.fromstring(s, dtype=np.uint8).reshape((self._height, self._width, 4))
|
|
||||||
|
|
||||||
return True, arr
|
|
|
@ -1,293 +0,0 @@
|
||||||
from platform import system
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from util import Singleton, curr_directory, log, copy, append_slash
|
|
||||||
import pyaudio
|
|
||||||
from toxes import ToxES
|
|
||||||
import smileys
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(dict, Singleton):
|
|
||||||
"""
|
|
||||||
Settings of current profile + global app settings
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name):
|
|
||||||
Singleton.__init__(self)
|
|
||||||
self.path = ProfileHelper.get_path() + str(name) + '.json'
|
|
||||||
self.name = name
|
|
||||||
if os.path.isfile(self.path):
|
|
||||||
with open(self.path, 'rb') as fl:
|
|
||||||
data = fl.read()
|
|
||||||
inst = ToxES.get_instance()
|
|
||||||
try:
|
|
||||||
if inst.is_data_encrypted(data):
|
|
||||||
data = inst.pass_decrypt(data)
|
|
||||||
info = json.loads(str(data, 'utf-8'))
|
|
||||||
except Exception as ex:
|
|
||||||
info = Settings.get_default_settings()
|
|
||||||
log('Parsing settings error: ' + str(ex))
|
|
||||||
super(Settings, self).__init__(info)
|
|
||||||
self.upgrade()
|
|
||||||
else:
|
|
||||||
super(Settings, self).__init__(Settings.get_default_settings())
|
|
||||||
self.save()
|
|
||||||
smileys.SmileyLoader(self)
|
|
||||||
self.locked = False
|
|
||||||
self.closing = False
|
|
||||||
self.unlockScreen = False
|
|
||||||
p = pyaudio.PyAudio()
|
|
||||||
input_devices = output_devices = 0
|
|
||||||
for i in range(p.get_device_count()):
|
|
||||||
device = p.get_device_info_by_index(i)
|
|
||||||
if device["maxInputChannels"]:
|
|
||||||
input_devices += 1
|
|
||||||
if device["maxOutputChannels"]:
|
|
||||||
output_devices += 1
|
|
||||||
self.audio = {'input': p.get_default_input_device_info()['index'] if input_devices else -1,
|
|
||||||
'output': p.get_default_output_device_info()['index'] if output_devices else -1,
|
|
||||||
'enabled': input_devices and output_devices}
|
|
||||||
self.video = {'device': -1, 'width': 640, 'height': 480, 'x': 0, 'y': 0}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_auto_profile():
|
|
||||||
p = Settings.get_global_settings_path()
|
|
||||||
if os.path.isfile(p):
|
|
||||||
with open(p) as fl:
|
|
||||||
data = fl.read()
|
|
||||||
auto = json.loads(data)
|
|
||||||
if 'path' in auto and 'name' in auto:
|
|
||||||
path = str(auto['path'])
|
|
||||||
name = str(auto['name'])
|
|
||||||
if os.path.isfile(append_slash(path) + name + '.tox'):
|
|
||||||
return path, name
|
|
||||||
return '', ''
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_auto_profile(path, name):
|
|
||||||
p = Settings.get_global_settings_path()
|
|
||||||
if os.path.isfile(p):
|
|
||||||
with open(p) as fl:
|
|
||||||
data = fl.read()
|
|
||||||
data = json.loads(data)
|
|
||||||
else:
|
|
||||||
data = {}
|
|
||||||
data['path'] = str(path)
|
|
||||||
data['name'] = str(name)
|
|
||||||
with open(p, 'w') as fl:
|
|
||||||
fl.write(json.dumps(data))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def reset_auto_profile():
|
|
||||||
p = Settings.get_global_settings_path()
|
|
||||||
if os.path.isfile(p):
|
|
||||||
with open(p) as fl:
|
|
||||||
data = fl.read()
|
|
||||||
data = json.loads(data)
|
|
||||||
else:
|
|
||||||
data = {}
|
|
||||||
if 'path' in data:
|
|
||||||
del data['path']
|
|
||||||
del data['name']
|
|
||||||
with open(p, 'w') as fl:
|
|
||||||
fl.write(json.dumps(data))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_active_profile(path, name):
|
|
||||||
path = path + name + '.lock'
|
|
||||||
return os.path.isfile(path)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_default_settings():
|
|
||||||
"""
|
|
||||||
Default profile settings
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'theme': 'dark',
|
|
||||||
'ipv6_enabled': False,
|
|
||||||
'udp_enabled': True,
|
|
||||||
'proxy_type': 0,
|
|
||||||
'proxy_host': '127.0.0.1',
|
|
||||||
'proxy_port': 9050,
|
|
||||||
'start_port': 0,
|
|
||||||
'end_port': 0,
|
|
||||||
'tcp_port': 0,
|
|
||||||
'notifications': True,
|
|
||||||
'sound_notifications': False,
|
|
||||||
'language': 'English',
|
|
||||||
'save_history': False,
|
|
||||||
'allow_inline': True,
|
|
||||||
'allow_auto_accept': True,
|
|
||||||
'auto_accept_path': None,
|
|
||||||
'sorting': 0,
|
|
||||||
'auto_accept_from_friends': [],
|
|
||||||
'paused_file_transfers': {},
|
|
||||||
'resend_files': True,
|
|
||||||
'friends_aliases': [],
|
|
||||||
'show_avatars': False,
|
|
||||||
'typing_notifications': False,
|
|
||||||
'calls_sound': True,
|
|
||||||
'blocked': [],
|
|
||||||
'plugins': [],
|
|
||||||
'notes': {},
|
|
||||||
'smileys': True,
|
|
||||||
'smiley_pack': 'default',
|
|
||||||
'mirror_mode': False,
|
|
||||||
'width': 920,
|
|
||||||
'height': 500,
|
|
||||||
'x': 400,
|
|
||||||
'y': 400,
|
|
||||||
'message_font_size': 14,
|
|
||||||
'unread_color': 'red',
|
|
||||||
'save_unsent_only': False,
|
|
||||||
'compact_mode': False,
|
|
||||||
'show_welcome_screen': True,
|
|
||||||
'close_to_tray': False,
|
|
||||||
'font': 'Times New Roman',
|
|
||||||
'update': 1,
|
|
||||||
'group_notifications': True,
|
|
||||||
'download_nodes_list': False
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def supported_languages():
|
|
||||||
return {
|
|
||||||
'English': 'en_EN',
|
|
||||||
'French': 'fr_FR',
|
|
||||||
'Russian': 'ru_RU',
|
|
||||||
'Ukrainian': 'uk_UA'
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def built_in_themes():
|
|
||||||
return {
|
|
||||||
'dark': '/styles/dark_style.qss',
|
|
||||||
'default': '/styles/style.qss'
|
|
||||||
}
|
|
||||||
|
|
||||||
def upgrade(self):
|
|
||||||
default = Settings.get_default_settings()
|
|
||||||
for key in default:
|
|
||||||
if key not in self:
|
|
||||||
print(key)
|
|
||||||
self[key] = default[key]
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
text = json.dumps(self)
|
|
||||||
inst = ToxES.get_instance()
|
|
||||||
if inst.has_password():
|
|
||||||
text = bytes(inst.pass_encrypt(bytes(text, 'utf-8')))
|
|
||||||
else:
|
|
||||||
text = bytes(text, 'utf-8')
|
|
||||||
with open(self.path, 'wb') as fl:
|
|
||||||
fl.write(text)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
profile_path = ProfileHelper.get_path()
|
|
||||||
path = str(profile_path + str(self.name) + '.lock')
|
|
||||||
if os.path.isfile(path):
|
|
||||||
os.remove(path)
|
|
||||||
|
|
||||||
def set_active_profile(self):
|
|
||||||
"""
|
|
||||||
Mark current profile as active
|
|
||||||
"""
|
|
||||||
profile_path = ProfileHelper.get_path()
|
|
||||||
path = str(profile_path + str(self.name) + '.lock')
|
|
||||||
with open(path, 'w') as fl:
|
|
||||||
fl.write('active')
|
|
||||||
|
|
||||||
def export(self, path):
|
|
||||||
text = json.dumps(self)
|
|
||||||
with open(path + str(self.name) + '.json', 'w') as fl:
|
|
||||||
fl.write(text)
|
|
||||||
|
|
||||||
def update_path(self):
|
|
||||||
self.path = ProfileHelper.get_path() + self.name + '.json'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_global_settings_path():
|
|
||||||
return curr_directory() + '/toxygen.json'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_default_path():
|
|
||||||
if system() == 'Windows':
|
|
||||||
return os.getenv('APPDATA') + '/Tox/'
|
|
||||||
elif system() == 'Darwin':
|
|
||||||
return os.getenv('HOME') + '/Library/Application Support/Tox/'
|
|
||||||
else:
|
|
||||||
return os.getenv('HOME') + '/.config/tox/'
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileHelper(Singleton):
|
|
||||||
"""
|
|
||||||
Class with methods for search, load and save profiles
|
|
||||||
"""
|
|
||||||
def __init__(self, path, name):
|
|
||||||
Singleton.__init__(self)
|
|
||||||
path = append_slash(path)
|
|
||||||
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 = ToxES.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, use_new_path):
|
|
||||||
path = new_path + os.path.basename(self._path)
|
|
||||||
with open(self._path, 'rb') as fin:
|
|
||||||
data = fin.read()
|
|
||||||
with open(path, 'wb') as fout:
|
|
||||||
fout.write(data)
|
|
||||||
print('Profile exported successfully')
|
|
||||||
copy(self._directory + 'avatars', new_path + 'avatars')
|
|
||||||
if use_new_path:
|
|
||||||
self._path = new_path + os.path.basename(self._path)
|
|
||||||
self._directory = new_path
|
|
||||||
Settings.get_instance().update_path()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def find_profiles():
|
|
||||||
"""
|
|
||||||
Find available tox profiles
|
|
||||||
"""
|
|
||||||
path = Settings.get_default_path()
|
|
||||||
result = []
|
|
||||||
# check default path
|
|
||||||
if not os.path.exists(path):
|
|
||||||
os.makedirs(path)
|
|
||||||
for fl in os.listdir(path):
|
|
||||||
if fl.endswith('.tox'):
|
|
||||||
name = fl[:-4]
|
|
||||||
result.append((path, name))
|
|
||||||
path = curr_directory()
|
|
||||||
# check current directory
|
|
||||||
for fl in os.listdir(path):
|
|
||||||
if fl.endswith('.tox'):
|
|
||||||
name = fl[:-4]
|
|
||||||
result.append((path + '/', name))
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_path():
|
|
||||||
return ProfileHelper.get_instance().get_dir()
|
|
|
@ -1,88 +0,0 @@
|
||||||
import util
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from collections import OrderedDict
|
|
||||||
from PyQt5 import QtCore
|
|
||||||
|
|
||||||
|
|
||||||
class SmileyLoader(util.Singleton):
|
|
||||||
"""
|
|
||||||
Class which loads smileys packs and insert smileys into messages
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, settings):
|
|
||||||
super().__init__()
|
|
||||||
self._settings = settings
|
|
||||||
self._curr_pack = None # current pack name
|
|
||||||
self._smileys = {} # smileys dict. key - smiley (str), value - path to image (str)
|
|
||||||
self._list = [] # smileys list without duplicates
|
|
||||||
self.load_pack()
|
|
||||||
|
|
||||||
def load_pack(self):
|
|
||||||
"""
|
|
||||||
Loads smiley pack
|
|
||||||
"""
|
|
||||||
pack_name = self._settings['smiley_pack']
|
|
||||||
if self._settings['smileys'] and self._curr_pack != pack_name:
|
|
||||||
self._curr_pack = pack_name
|
|
||||||
path = self.get_smileys_path() + 'config.json'
|
|
||||||
try:
|
|
||||||
with open(path, encoding='utf8') as fl:
|
|
||||||
self._smileys = json.loads(fl.read())
|
|
||||||
fl.seek(0)
|
|
||||||
tmp = json.loads(fl.read(), object_pairs_hook=OrderedDict)
|
|
||||||
print('Smiley pack {} loaded'.format(pack_name))
|
|
||||||
keys, values, self._list = [], [], []
|
|
||||||
for key, value in tmp.items():
|
|
||||||
value = self.get_smileys_path() + value
|
|
||||||
if value not in values:
|
|
||||||
keys.append(key)
|
|
||||||
values.append(value)
|
|
||||||
self._list = list(zip(keys, values))
|
|
||||||
except Exception as ex:
|
|
||||||
self._smileys = {}
|
|
||||||
self._list = []
|
|
||||||
print('Smiley pack {} was not loaded. Error: {}'.format(pack_name, ex))
|
|
||||||
|
|
||||||
def get_smileys_path(self):
|
|
||||||
return util.curr_directory() + '/smileys/' + self._curr_pack + '/' if self._curr_pack is not None else None
|
|
||||||
|
|
||||||
def get_packs_list(self):
|
|
||||||
d = util.curr_directory() + '/smileys/'
|
|
||||||
return [x[1] for x in os.walk(d)][0]
|
|
||||||
|
|
||||||
def get_smileys(self):
|
|
||||||
return list(self._list)
|
|
||||||
|
|
||||||
def add_smileys_to_text(self, text, edit):
|
|
||||||
"""
|
|
||||||
Adds smileys to text
|
|
||||||
:param text: message
|
|
||||||
:param edit: MessageEdit instance
|
|
||||||
:return text with smileys
|
|
||||||
"""
|
|
||||||
if not self._settings['smileys'] or not len(self._smileys):
|
|
||||||
return text
|
|
||||||
arr = text.split(' ')
|
|
||||||
for i in range(len(arr)):
|
|
||||||
if arr[i] in self._smileys:
|
|
||||||
file_name = self._smileys[arr[i]] # image name
|
|
||||||
arr[i] = '<img title=\"{}\" src=\"{}\" />'.format(arr[i], file_name)
|
|
||||||
if file_name.endswith('.gif'): # animated smiley
|
|
||||||
edit.addAnimation(QtCore.QUrl(file_name), self.get_smileys_path() + file_name)
|
|
||||||
return ' '.join(arr)
|
|
||||||
|
|
||||||
|
|
||||||
def sticker_loader():
|
|
||||||
"""
|
|
||||||
:return list of stickers
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
d = util.curr_directory() + '/stickers/'
|
|
||||||
keys = [x[1] for x in os.walk(d)][0]
|
|
||||||
for key in keys:
|
|
||||||
path = d + key + '/'
|
|
||||||
files = filter(lambda f: f.endswith('.png'), os.listdir(path))
|
|
||||||
files = map(lambda f: str(path + f), files)
|
|
||||||
result.extend(files)
|
|
||||||
return result
|
|
1601
toxygen/tox.py
1601
toxygen/tox.py
File diff suppressed because it is too large
Load diff
|
@ -1,59 +0,0 @@
|
||||||
import json
|
|
||||||
import urllib.request
|
|
||||||
from util import log
|
|
||||||
import settings
|
|
||||||
from PyQt5 import QtNetwork, QtCore
|
|
||||||
|
|
||||||
|
|
||||||
def tox_dns(email):
|
|
||||||
"""
|
|
||||||
TOX DNS 4
|
|
||||||
:param email: data like 'groupbot@toxme.io'
|
|
||||||
:return: tox id on success else None
|
|
||||||
"""
|
|
||||||
site = email.split('@')[1]
|
|
||||||
data = {"action": 3, "name": "{}".format(email)}
|
|
||||||
urls = ('https://{}/api'.format(site), 'http://{}/api'.format(site))
|
|
||||||
s = settings.Settings.get_instance()
|
|
||||||
if not s['proxy_type']: # no proxy
|
|
||||||
for url in urls:
|
|
||||||
try:
|
|
||||||
return send_request(url, data)
|
|
||||||
except Exception as ex:
|
|
||||||
log('TOX DNS ERROR: ' + str(ex))
|
|
||||||
else: # proxy
|
|
||||||
netman = QtNetwork.QNetworkAccessManager()
|
|
||||||
proxy = QtNetwork.QNetworkProxy()
|
|
||||||
proxy.setType(QtNetwork.QNetworkProxy.Socks5Proxy if s['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy)
|
|
||||||
proxy.setHostName(s['proxy_host'])
|
|
||||||
proxy.setPort(s['proxy_port'])
|
|
||||||
netman.setProxy(proxy)
|
|
||||||
for url in urls:
|
|
||||||
try:
|
|
||||||
request = QtNetwork.QNetworkRequest()
|
|
||||||
request.setUrl(QtCore.QUrl(url))
|
|
||||||
request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/json")
|
|
||||||
reply = netman.post(request, bytes(json.dumps(data), 'utf-8'))
|
|
||||||
|
|
||||||
while not reply.isFinished():
|
|
||||||
QtCore.QThread.msleep(1)
|
|
||||||
QtCore.QCoreApplication.processEvents()
|
|
||||||
data = bytes(reply.readAll().data())
|
|
||||||
result = json.loads(str(data, 'utf-8'))
|
|
||||||
if not result['c']:
|
|
||||||
return result['tox_id']
|
|
||||||
except Exception as ex:
|
|
||||||
log('TOX DNS ERROR: ' + str(ex))
|
|
||||||
|
|
||||||
return None # error
|
|
||||||
|
|
||||||
|
|
||||||
def send_request(url, data):
|
|
||||||
req = urllib.request.Request(url)
|
|
||||||
req.add_header('Content-Type', 'application/json')
|
|
||||||
response = urllib.request.urlopen(req, bytes(json.dumps(data), 'utf-8'))
|
|
||||||
res = json.loads(str(response.read(), 'utf-8'))
|
|
||||||
if not res['c']:
|
|
||||||
return res['tox_id']
|
|
||||||
else:
|
|
||||||
raise LookupError()
|
|
362
toxygen/toxav.py
362
toxygen/toxav.py
|
@ -1,362 +0,0 @@
|
||||||
from ctypes import c_int, POINTER, c_void_p, byref, ArgumentError, c_uint32, CFUNCTYPE, c_size_t, c_uint8, c_uint16
|
|
||||||
from ctypes import c_char_p, c_int32, c_bool, cast
|
|
||||||
from libtox import LibToxAV
|
|
||||||
from toxav_enums import *
|
|
||||||
|
|
||||||
|
|
||||||
class ToxAV:
|
|
||||||
"""
|
|
||||||
The ToxAV instance type. Each ToxAV instance can be bound to only one Tox instance, and Tox instance can have only
|
|
||||||
one ToxAV instance. One must make sure to close ToxAV instance prior closing Tox instance otherwise undefined
|
|
||||||
behaviour occurs. Upon closing of ToxAV instance, all active calls will be forcibly terminated without notifying
|
|
||||||
peers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Creation and destruction
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def __init__(self, tox_pointer):
|
|
||||||
"""
|
|
||||||
Start new A/V session. There can only be only one session per Tox instance.
|
|
||||||
|
|
||||||
:param tox_pointer: pointer to Tox instance
|
|
||||||
"""
|
|
||||||
self.libtoxav = LibToxAV()
|
|
||||||
toxav_err_new = c_int()
|
|
||||||
self.libtoxav.toxav_new.restype = POINTER(c_void_p)
|
|
||||||
self._toxav_pointer = self.libtoxav.toxav_new(tox_pointer, byref(toxav_err_new))
|
|
||||||
toxav_err_new = toxav_err_new.value
|
|
||||||
if toxav_err_new == TOXAV_ERR_NEW['NULL']:
|
|
||||||
raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
|
|
||||||
elif toxav_err_new == TOXAV_ERR_NEW['MALLOC']:
|
|
||||||
raise MemoryError('Memory allocation failure while trying to allocate structures required for the A/V '
|
|
||||||
'session.')
|
|
||||||
elif toxav_err_new == TOXAV_ERR_NEW['MULTIPLE']:
|
|
||||||
raise RuntimeError('Attempted to create a second session for the same Tox instance.')
|
|
||||||
|
|
||||||
self.call_state_cb = None
|
|
||||||
self.audio_receive_frame_cb = None
|
|
||||||
self.video_receive_frame_cb = None
|
|
||||||
self.call_cb = None
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
"""
|
|
||||||
Releases all resources associated with the A/V session.
|
|
||||||
|
|
||||||
If any calls were ongoing, these will be forcibly terminated without notifying peers. After calling this
|
|
||||||
function, no other functions may be called and the av pointer becomes invalid.
|
|
||||||
"""
|
|
||||||
self.libtoxav.toxav_kill(self._toxav_pointer)
|
|
||||||
|
|
||||||
def get_tox_pointer(self):
|
|
||||||
"""
|
|
||||||
Returns the Tox instance the A/V object was created for.
|
|
||||||
|
|
||||||
:return: pointer to the Tox instance
|
|
||||||
"""
|
|
||||||
self.libtoxav.toxav_get_tox.restype = POINTER(c_void_p)
|
|
||||||
return self.libtoxav.toxav_get_tox(self._toxav_pointer)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# A/V event loop
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def iteration_interval(self):
|
|
||||||
"""
|
|
||||||
Returns the interval in milliseconds when the next toxav_iterate call should be. If no call is active at the
|
|
||||||
moment, this function returns 200.
|
|
||||||
|
|
||||||
:return: interval in milliseconds
|
|
||||||
"""
|
|
||||||
return self.libtoxav.toxav_iteration_interval(self._toxav_pointer)
|
|
||||||
|
|
||||||
def iterate(self):
|
|
||||||
"""
|
|
||||||
Main loop for the session. This function needs to be called in intervals of toxav_iteration_interval()
|
|
||||||
milliseconds. It is best called in the separate thread from tox_iterate.
|
|
||||||
"""
|
|
||||||
self.libtoxav.toxav_iterate(self._toxav_pointer)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Call setup
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def call(self, friend_number, audio_bit_rate, video_bit_rate):
|
|
||||||
"""
|
|
||||||
Call a friend. This will start ringing the friend.
|
|
||||||
|
|
||||||
It is the client's responsibility to stop ringing after a certain timeout, if such behaviour is desired. If the
|
|
||||||
client does not stop ringing, the library will not stop until the friend is disconnected. Audio and video
|
|
||||||
receiving are both enabled by default.
|
|
||||||
|
|
||||||
:param friend_number: The friend number of the friend that should be called.
|
|
||||||
:param audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending.
|
|
||||||
:param video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending.
|
|
||||||
:return: True on success.
|
|
||||||
"""
|
|
||||||
toxav_err_call = c_int()
|
|
||||||
result = self.libtoxav.toxav_call(self._toxav_pointer, c_uint32(friend_number), c_uint32(audio_bit_rate),
|
|
||||||
c_uint32(video_bit_rate), byref(toxav_err_call))
|
|
||||||
toxav_err_call = toxav_err_call.value
|
|
||||||
if toxav_err_call == TOXAV_ERR_CALL['OK']:
|
|
||||||
return bool(result)
|
|
||||||
elif toxav_err_call == TOXAV_ERR_CALL['MALLOC']:
|
|
||||||
raise MemoryError('A resource allocation error occurred while trying to create the structures required for '
|
|
||||||
'the call.')
|
|
||||||
elif toxav_err_call == TOXAV_ERR_CALL['SYNC']:
|
|
||||||
raise RuntimeError('Synchronization error occurred.')
|
|
||||||
elif toxav_err_call == TOXAV_ERR_CALL['FRIEND_NOT_FOUND']:
|
|
||||||
raise ArgumentError('The friend number did not designate a valid friend.')
|
|
||||||
elif toxav_err_call == TOXAV_ERR_CALL['FRIEND_NOT_CONNECTED']:
|
|
||||||
raise ArgumentError('The friend was valid, but not currently connected.')
|
|
||||||
elif toxav_err_call == TOXAV_ERR_CALL['FRIEND_ALREADY_IN_CALL']:
|
|
||||||
raise ArgumentError('Attempted to call a friend while already in an audio or video call with them.')
|
|
||||||
elif toxav_err_call == TOXAV_ERR_CALL['INVALID_BIT_RATE']:
|
|
||||||
raise ArgumentError('Audio or video bit rate is invalid.')
|
|
||||||
|
|
||||||
def callback_call(self, callback, user_data):
|
|
||||||
"""
|
|
||||||
Set the callback for the `call` event. Pass None to unset.
|
|
||||||
|
|
||||||
:param callback: The function for the call callback.
|
|
||||||
|
|
||||||
Should take pointer (c_void_p) to ToxAV object,
|
|
||||||
The friend number (c_uint32) from which the call is incoming.
|
|
||||||
True (c_bool) if friend is sending audio.
|
|
||||||
True (c_bool) if friend is sending video.
|
|
||||||
pointer (c_void_p) to user_data
|
|
||||||
:param user_data: pointer (c_void_p) to user data
|
|
||||||
"""
|
|
||||||
c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_bool, c_bool, c_void_p)
|
|
||||||
self.call_cb = c_callback(callback)
|
|
||||||
self.libtoxav.toxav_callback_call(self._toxav_pointer, self.call_cb, user_data)
|
|
||||||
|
|
||||||
def answer(self, friend_number, audio_bit_rate, video_bit_rate):
|
|
||||||
"""
|
|
||||||
Accept an incoming call.
|
|
||||||
|
|
||||||
If answering fails for any reason, the call will still be pending and it is possible to try and answer it later.
|
|
||||||
Audio and video receiving are both enabled by default.
|
|
||||||
|
|
||||||
:param friend_number: The friend number of the friend that is calling.
|
|
||||||
:param audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending.
|
|
||||||
:param video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending.
|
|
||||||
:return: True on success.
|
|
||||||
"""
|
|
||||||
toxav_err_answer = c_int()
|
|
||||||
result = self.libtoxav.toxav_answer(self._toxav_pointer, c_uint32(friend_number), c_uint32(audio_bit_rate),
|
|
||||||
c_uint32(video_bit_rate), byref(toxav_err_answer))
|
|
||||||
toxav_err_answer = toxav_err_answer.value
|
|
||||||
if toxav_err_answer == TOXAV_ERR_ANSWER['OK']:
|
|
||||||
return bool(result)
|
|
||||||
elif toxav_err_answer == TOXAV_ERR_ANSWER['SYNC']:
|
|
||||||
raise RuntimeError('Synchronization error occurred.')
|
|
||||||
elif toxav_err_answer == TOXAV_ERR_ANSWER['CODEC_INITIALIZATION']:
|
|
||||||
raise RuntimeError('Failed to initialize codecs for call session. Note that codec initiation will fail if '
|
|
||||||
'there is no receive callback registered for either audio or video.')
|
|
||||||
elif toxav_err_answer == TOXAV_ERR_ANSWER['FRIEND_NOT_FOUND']:
|
|
||||||
raise ArgumentError('The friend number did not designate a valid friend.')
|
|
||||||
elif toxav_err_answer == TOXAV_ERR_ANSWER['FRIEND_NOT_CALLING']:
|
|
||||||
raise ArgumentError('The friend was valid, but they are not currently trying to initiate a call. This is '
|
|
||||||
'also returned if this client is already in a call with the friend.')
|
|
||||||
elif toxav_err_answer == TOXAV_ERR_ANSWER['INVALID_BIT_RATE']:
|
|
||||||
raise ArgumentError('Audio or video bit rate is invalid.')
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Call state graph
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def callback_call_state(self, callback, user_data):
|
|
||||||
"""
|
|
||||||
Set the callback for the `call_state` event. Pass None to unset.
|
|
||||||
|
|
||||||
:param callback: Python function.
|
|
||||||
The function for the call_state callback.
|
|
||||||
|
|
||||||
Should take pointer (c_void_p) to ToxAV object,
|
|
||||||
The friend number (c_uint32) for which the call state changed.
|
|
||||||
The bitmask of the new call state which is guaranteed to be different than the previous state. The state is set
|
|
||||||
to 0 when the call is paused. The bitmask represents all the activities currently performed by the friend.
|
|
||||||
pointer (c_void_p) to user_data
|
|
||||||
:param user_data: pointer (c_void_p) to user data
|
|
||||||
"""
|
|
||||||
c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p)
|
|
||||||
self.call_state_cb = c_callback(callback)
|
|
||||||
self.libtoxav.toxav_callback_call_state(self._toxav_pointer, self.call_state_cb, user_data)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# Call control
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def call_control(self, friend_number, control):
|
|
||||||
"""
|
|
||||||
Sends a call control command to a friend.
|
|
||||||
|
|
||||||
:param friend_number: The friend number of the friend this client is in a call with.
|
|
||||||
:param control: The control command to send.
|
|
||||||
:return: True on success.
|
|
||||||
"""
|
|
||||||
toxav_err_call_control = c_int()
|
|
||||||
result = self.libtoxav.toxav_call_control(self._toxav_pointer, c_uint32(friend_number), c_int(control),
|
|
||||||
byref(toxav_err_call_control))
|
|
||||||
toxav_err_call_control = toxav_err_call_control.value
|
|
||||||
if toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['OK']:
|
|
||||||
return bool(result)
|
|
||||||
elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['SYNC']:
|
|
||||||
raise RuntimeError('Synchronization error occurred.')
|
|
||||||
elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['FRIEND_NOT_FOUND']:
|
|
||||||
raise ArgumentError('The friend_number passed did not designate a valid friend.')
|
|
||||||
elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['FRIEND_NOT_IN_CALL']:
|
|
||||||
raise RuntimeError('This client is currently not in a call with the friend. Before the call is answered, '
|
|
||||||
'only CANCEL is a valid control.')
|
|
||||||
elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['INVALID_TRANSITION']:
|
|
||||||
raise RuntimeError('Happens if user tried to pause an already paused call or if trying to resume a call '
|
|
||||||
'that is not paused.')
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# TODO Controlling bit rates
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# A/V sending
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def audio_send_frame(self, friend_number, pcm, sample_count, channels, sampling_rate):
|
|
||||||
"""
|
|
||||||
Send an audio frame to a friend.
|
|
||||||
|
|
||||||
The expected format of the PCM data is: [s1c1][s1c2][...][s2c1][s2c2][...]...
|
|
||||||
Meaning: sample 1 for channel 1, sample 1 for channel 2, ...
|
|
||||||
For mono audio, this has no meaning, every sample is subsequent. For stereo, this means the expected format is
|
|
||||||
LRLRLR... with samples for left and right alternating.
|
|
||||||
|
|
||||||
:param friend_number: The friend number of the friend to which to send an audio frame.
|
|
||||||
:param pcm: An array of audio samples. The size of this array must be sample_count * channels.
|
|
||||||
:param sample_count: Number of samples in this frame. Valid numbers here are
|
|
||||||
((sample rate) * (audio length) / 1000), where audio length can be 2.5, 5, 10, 20, 40 or 60 milliseconds.
|
|
||||||
:param channels: Number of audio channels. Sulpported values are 1 and 2.
|
|
||||||
:param sampling_rate: Audio sampling rate used in this frame. Valid sampling rates are 8000, 12000, 16000,
|
|
||||||
24000, or 48000.
|
|
||||||
"""
|
|
||||||
toxav_err_send_frame = c_int()
|
|
||||||
result = self.libtoxav.toxav_audio_send_frame(self._toxav_pointer, c_uint32(friend_number),
|
|
||||||
cast(pcm, c_void_p),
|
|
||||||
c_size_t(sample_count), c_uint8(channels),
|
|
||||||
c_uint32(sampling_rate), byref(toxav_err_send_frame))
|
|
||||||
toxav_err_send_frame = toxav_err_send_frame.value
|
|
||||||
if toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['OK']:
|
|
||||||
return bool(result)
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['NULL']:
|
|
||||||
raise ArgumentError('The samples data pointer was NULL.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_FOUND']:
|
|
||||||
raise ArgumentError('The friend_number passed did not designate a valid friend.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_IN_CALL']:
|
|
||||||
raise RuntimeError('This client is currently not in a call with the friend.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['SYNC']:
|
|
||||||
raise RuntimeError('Synchronization error occurred.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['INVALID']:
|
|
||||||
raise ArgumentError('One of the frame parameters was invalid. E.g. the resolution may be too small or too '
|
|
||||||
'large, or the audio sampling rate may be unsupported.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['PAYLOAD_TYPE_DISABLED']:
|
|
||||||
raise RuntimeError('Either friend turned off audio or video receiving or we turned off sending for the said'
|
|
||||||
'payload.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['RTP_FAILED']:
|
|
||||||
RuntimeError('Failed to push frame through rtp interface.')
|
|
||||||
|
|
||||||
def video_send_frame(self, friend_number, width, height, y, u, v):
|
|
||||||
"""
|
|
||||||
Send a video frame to a friend.
|
|
||||||
|
|
||||||
Y - plane should be of size: height * width
|
|
||||||
U - plane should be of size: (height/2) * (width/2)
|
|
||||||
V - plane should be of size: (height/2) * (width/2)
|
|
||||||
|
|
||||||
:param friend_number: The friend number of the friend to which to send a video frame.
|
|
||||||
:param width: Width of the frame in pixels.
|
|
||||||
:param height: Height of the frame in pixels.
|
|
||||||
:param y: Y (Luminance) plane data.
|
|
||||||
:param u: U (Chroma) plane data.
|
|
||||||
:param v: V (Chroma) plane data.
|
|
||||||
"""
|
|
||||||
toxav_err_send_frame = c_int()
|
|
||||||
result = self.libtoxav.toxav_video_send_frame(self._toxav_pointer, c_uint32(friend_number), c_uint16(width),
|
|
||||||
c_uint16(height), c_char_p(y), c_char_p(u), c_char_p(v),
|
|
||||||
byref(toxav_err_send_frame))
|
|
||||||
toxav_err_send_frame = toxav_err_send_frame.value
|
|
||||||
if toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['OK']:
|
|
||||||
return bool(result)
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['NULL']:
|
|
||||||
raise ArgumentError('One of Y, U, or V was NULL.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_FOUND']:
|
|
||||||
raise ArgumentError('The friend_number passed did not designate a valid friend.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_IN_CALL']:
|
|
||||||
raise RuntimeError('This client is currently not in a call with the friend.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['SYNC']:
|
|
||||||
raise RuntimeError('Synchronization error occurred.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['INVALID']:
|
|
||||||
raise ArgumentError('One of the frame parameters was invalid. E.g. the resolution may be too small or too '
|
|
||||||
'large, or the audio sampling rate may be unsupported.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['PAYLOAD_TYPE_DISABLED']:
|
|
||||||
raise RuntimeError('Either friend turned off audio or video receiving or we turned off sending for the said'
|
|
||||||
'payload.')
|
|
||||||
elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['RTP_FAILED']:
|
|
||||||
RuntimeError('Failed to push frame through rtp interface.')
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
# A/V receiving
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def callback_audio_receive_frame(self, callback, user_data):
|
|
||||||
"""
|
|
||||||
Set the callback for the `audio_receive_frame` event. Pass None to unset.
|
|
||||||
|
|
||||||
:param callback: Python function.
|
|
||||||
Function for the audio_receive_frame callback. The callback can be called multiple times per single
|
|
||||||
iteration depending on the amount of queued frames in the buffer. The received format is the same as in send
|
|
||||||
function.
|
|
||||||
|
|
||||||
Should take pointer (c_void_p) to ToxAV object,
|
|
||||||
The friend number (c_uint32) of the friend who sent an audio frame.
|
|
||||||
An array (c_uint8) of audio samples (sample_count * channels elements).
|
|
||||||
The number (c_size_t) of audio samples per channel in the PCM array.
|
|
||||||
Number (c_uint8) of audio channels.
|
|
||||||
Sampling rate (c_uint32) used in this frame.
|
|
||||||
pointer (c_void_p) to user_data
|
|
||||||
:param user_data: pointer (c_void_p) to user data
|
|
||||||
"""
|
|
||||||
c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, c_uint8, c_uint32, c_void_p)
|
|
||||||
self.audio_receive_frame_cb = c_callback(callback)
|
|
||||||
self.libtoxav.toxav_callback_audio_receive_frame(self._toxav_pointer, self.audio_receive_frame_cb, user_data)
|
|
||||||
|
|
||||||
def callback_video_receive_frame(self, callback, user_data):
|
|
||||||
"""
|
|
||||||
Set the callback for the `video_receive_frame` event. Pass None to unset.
|
|
||||||
|
|
||||||
:param callback: Python function.
|
|
||||||
The function type for the video_receive_frame callback.
|
|
||||||
|
|
||||||
Should take
|
|
||||||
toxAV pointer (c_void_p) to ToxAV object,
|
|
||||||
friend_number The friend number (c_uint32) of the friend who sent a video frame.
|
|
||||||
width Width (c_uint16) of the frame in pixels.
|
|
||||||
height Height (c_uint16) of the frame in pixels.
|
|
||||||
y
|
|
||||||
u
|
|
||||||
v Plane data (POINTER(c_uint8)).
|
|
||||||
The size of plane data is derived from width and height where
|
|
||||||
Y = MAX(width, abs(ystride)) * height,
|
|
||||||
U = MAX(width/2, abs(ustride)) * (height/2) and
|
|
||||||
V = MAX(width/2, abs(vstride)) * (height/2).
|
|
||||||
ystride
|
|
||||||
ustride
|
|
||||||
vstride Strides data (c_int32). Strides represent padding for each plane that may or may not be present. You must
|
|
||||||
handle strides in your image processing code. Strides are negative if the image is bottom-up
|
|
||||||
hence why you MUST abs() it when calculating plane buffer size.
|
|
||||||
user_data pointer (c_void_p) to user_data
|
|
||||||
:param user_data: pointer (c_void_p) to user data
|
|
||||||
"""
|
|
||||||
c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint16, c_uint16, POINTER(c_uint8), POINTER(c_uint8),
|
|
||||||
POINTER(c_uint8), c_int32, c_int32, c_int32, c_void_p)
|
|
||||||
self.video_receive_frame_cb = c_callback(callback)
|
|
||||||
self.libtoxav.toxav_callback_video_receive_frame(self._toxav_pointer, self.video_receive_frame_cb, user_data)
|
|
|
@ -1,131 +0,0 @@
|
||||||
TOXAV_ERR_NEW = {
|
|
||||||
# The function returned successfully.
|
|
||||||
'OK': 0,
|
|
||||||
# One of the arguments to the function was NULL when it was not expected.
|
|
||||||
'NULL': 1,
|
|
||||||
# Memory allocation failure while trying to allocate structures required for the A/V session.
|
|
||||||
'MALLOC': 2,
|
|
||||||
# Attempted to create a second session for the same Tox instance.
|
|
||||||
'MULTIPLE': 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOXAV_ERR_CALL = {
|
|
||||||
# The function returned successfully.
|
|
||||||
'OK': 0,
|
|
||||||
# A resource allocation error occurred while trying to create the structures required for the call.
|
|
||||||
'MALLOC': 1,
|
|
||||||
# Synchronization error occurred.
|
|
||||||
'SYNC': 2,
|
|
||||||
# The friend number did not designate a valid friend.
|
|
||||||
'FRIEND_NOT_FOUND': 3,
|
|
||||||
# The friend was valid, but not currently connected.
|
|
||||||
'FRIEND_NOT_CONNECTED': 4,
|
|
||||||
# Attempted to call a friend while already in an audio or video call with them.
|
|
||||||
'FRIEND_ALREADY_IN_CALL': 5,
|
|
||||||
# Audio or video bit rate is invalid.
|
|
||||||
'INVALID_BIT_RATE': 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOXAV_ERR_ANSWER = {
|
|
||||||
# The function returned successfully.
|
|
||||||
'OK': 0,
|
|
||||||
# Synchronization error occurred.
|
|
||||||
'SYNC': 1,
|
|
||||||
# Failed to initialize codecs for call session. Note that codec initiation will fail if there is no receive callback
|
|
||||||
# registered for either audio or video.
|
|
||||||
'CODEC_INITIALIZATION': 2,
|
|
||||||
# The friend number did not designate a valid friend.
|
|
||||||
'FRIEND_NOT_FOUND': 3,
|
|
||||||
# The friend was valid, but they are not currently trying to initiate a call. This is also returned if this client
|
|
||||||
# is already in a call with the friend.
|
|
||||||
'FRIEND_NOT_CALLING': 4,
|
|
||||||
# Audio or video bit rate is invalid.
|
|
||||||
'INVALID_BIT_RATE': 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOXAV_FRIEND_CALL_STATE = {
|
|
||||||
# Set by the AV core if an error occurred on the remote end or if friend timed out. This is the final state after
|
|
||||||
# which no more state transitions can occur for the call. This call state will never be triggered in combination
|
|
||||||
# with other call states.
|
|
||||||
'ERROR': 1,
|
|
||||||
# The call has finished. This is the final state after which no more state transitions can occur for the call. This
|
|
||||||
# call state will never be triggered in combination with other call states.
|
|
||||||
'FINISHED': 2,
|
|
||||||
# The flag that marks that friend is sending audio.
|
|
||||||
'SENDING_A': 4,
|
|
||||||
# The flag that marks that friend is sending video.
|
|
||||||
'SENDING_V': 8,
|
|
||||||
# The flag that marks that friend is receiving audio.
|
|
||||||
'ACCEPTING_A': 16,
|
|
||||||
# The flag that marks that friend is receiving video.
|
|
||||||
'ACCEPTING_V': 32,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOXAV_CALL_CONTROL = {
|
|
||||||
# Resume a previously paused call. Only valid if the pause was caused by this client, if not, this control is
|
|
||||||
# ignored. Not valid before the call is accepted.
|
|
||||||
'RESUME': 0,
|
|
||||||
# Put a call on hold. Not valid before the call is accepted.
|
|
||||||
'PAUSE': 1,
|
|
||||||
# Reject a call if it was not answered, yet. Cancel a call after it was answered.
|
|
||||||
'CANCEL': 2,
|
|
||||||
# Request that the friend stops sending audio. Regardless of the friend's compliance, this will cause the
|
|
||||||
# audio_receive_frame event to stop being triggered on receiving an audio frame from the friend.
|
|
||||||
'MUTE_AUDIO': 3,
|
|
||||||
# Calling this control will notify client to start sending audio again.
|
|
||||||
'UNMUTE_AUDIO': 4,
|
|
||||||
# Request that the friend stops sending video. Regardless of the friend's compliance, this will cause the
|
|
||||||
# video_receive_frame event to stop being triggered on receiving a video frame from the friend.
|
|
||||||
'HIDE_VIDEO': 5,
|
|
||||||
# Calling this control will notify client to start sending video again.
|
|
||||||
'SHOW_VIDEO': 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOXAV_ERR_CALL_CONTROL = {
|
|
||||||
# The function returned successfully.
|
|
||||||
'OK': 0,
|
|
||||||
# Synchronization error occurred.
|
|
||||||
'SYNC': 1,
|
|
||||||
# The friend_number passed did not designate a valid friend.
|
|
||||||
'FRIEND_NOT_FOUND': 2,
|
|
||||||
# This client is currently not in a call with the friend. Before the call is answered, only CANCEL is a valid
|
|
||||||
# control.
|
|
||||||
'FRIEND_NOT_IN_CALL': 3,
|
|
||||||
# Happens if user tried to pause an already paused call or if trying to resume a call that is not paused.
|
|
||||||
'INVALID_TRANSITION': 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOXAV_ERR_BIT_RATE_SET = {
|
|
||||||
# The function returned successfully.
|
|
||||||
'OK': 0,
|
|
||||||
# Synchronization error occurred.
|
|
||||||
'SYNC': 1,
|
|
||||||
# The audio bit rate passed was not one of the supported values.
|
|
||||||
'INVALID_AUDIO_BIT_RATE': 2,
|
|
||||||
# The video bit rate passed was not one of the supported values.
|
|
||||||
'INVALID_VIDEO_BIT_RATE': 3,
|
|
||||||
# The friend_number passed did not designate a valid friend.
|
|
||||||
'FRIEND_NOT_FOUND': 4,
|
|
||||||
# This client is currently not in a call with the friend.
|
|
||||||
'FRIEND_NOT_IN_CALL': 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOXAV_ERR_SEND_FRAME = {
|
|
||||||
# The function returned successfully.
|
|
||||||
'OK': 0,
|
|
||||||
# In case of video, one of Y, U, or V was NULL. In case of audio, the samples data pointer was NULL.
|
|
||||||
'NULL': 1,
|
|
||||||
# The friend_number passed did not designate a valid friend.
|
|
||||||
'FRIEND_NOT_FOUND': 2,
|
|
||||||
# This client is currently not in a call with the friend.
|
|
||||||
'FRIEND_NOT_IN_CALL': 3,
|
|
||||||
# Synchronization error occurred.
|
|
||||||
'SYNC': 4,
|
|
||||||
# One of the frame parameters was invalid. E.g. the resolution may be too small or too large, or the audio sampling
|
|
||||||
# rate may be unsupported.
|
|
||||||
'INVALID': 5,
|
|
||||||
# Either friend turned off audio or video receiving or we turned off sending for the said payload.
|
|
||||||
'PAYLOAD_TYPE_DISABLED': 6,
|
|
||||||
# Failed to push frame through rtp interface.
|
|
||||||
'RTP_FAILED': 7,
|
|
||||||
}
|
|
|
@ -1,220 +0,0 @@
|
||||||
TOX_USER_STATUS = {
|
|
||||||
'NONE': 0,
|
|
||||||
'AWAY': 1,
|
|
||||||
'BUSY': 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_MESSAGE_TYPE = {
|
|
||||||
'NORMAL': 0,
|
|
||||||
'ACTION': 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_PROXY_TYPE = {
|
|
||||||
'NONE': 0,
|
|
||||||
'HTTP': 1,
|
|
||||||
'SOCKS5': 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_SAVEDATA_TYPE = {
|
|
||||||
'NONE': 0,
|
|
||||||
'TOX_SAVE': 1,
|
|
||||||
'SECRET_KEY': 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_OPTIONS_NEW = {
|
|
||||||
'OK': 0,
|
|
||||||
'MALLOC': 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_NEW = {
|
|
||||||
'OK': 0,
|
|
||||||
'NULL': 1,
|
|
||||||
'MALLOC': 2,
|
|
||||||
'PORT_ALLOC': 3,
|
|
||||||
'PROXY_BAD_TYPE': 4,
|
|
||||||
'PROXY_BAD_HOST': 5,
|
|
||||||
'PROXY_BAD_PORT': 6,
|
|
||||||
'PROXY_NOT_FOUND': 7,
|
|
||||||
'LOAD_ENCRYPTED': 8,
|
|
||||||
'LOAD_BAD_FORMAT': 9,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_BOOTSTRAP = {
|
|
||||||
'OK': 0,
|
|
||||||
'NULL': 1,
|
|
||||||
'BAD_HOST': 2,
|
|
||||||
'BAD_PORT': 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_CONNECTION = {
|
|
||||||
'NONE': 0,
|
|
||||||
'TCP': 1,
|
|
||||||
'UDP': 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_SET_INFO = {
|
|
||||||
'OK': 0,
|
|
||||||
'NULL': 1,
|
|
||||||
'TOO_LONG': 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FRIEND_ADD = {
|
|
||||||
'OK': 0,
|
|
||||||
'NULL': 1,
|
|
||||||
'TOO_LONG': 2,
|
|
||||||
'NO_MESSAGE': 3,
|
|
||||||
'OWN_KEY': 4,
|
|
||||||
'ALREADY_SENT': 5,
|
|
||||||
'BAD_CHECKSUM': 6,
|
|
||||||
'SET_NEW_NOSPAM': 7,
|
|
||||||
'MALLOC': 8,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FRIEND_DELETE = {
|
|
||||||
'OK': 0,
|
|
||||||
'FRIEND_NOT_FOUND': 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FRIEND_BY_PUBLIC_KEY = {
|
|
||||||
'OK': 0,
|
|
||||||
'NULL': 1,
|
|
||||||
'NOT_FOUND': 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FRIEND_GET_PUBLIC_KEY = {
|
|
||||||
'OK': 0,
|
|
||||||
'FRIEND_NOT_FOUND': 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FRIEND_GET_LAST_ONLINE = {
|
|
||||||
'OK': 0,
|
|
||||||
'FRIEND_NOT_FOUND': 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FRIEND_QUERY = {
|
|
||||||
'OK': 0,
|
|
||||||
'NULL': 1,
|
|
||||||
'FRIEND_NOT_FOUND': 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_SET_TYPING = {
|
|
||||||
'OK': 0,
|
|
||||||
'FRIEND_NOT_FOUND': 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FRIEND_SEND_MESSAGE = {
|
|
||||||
'OK': 0,
|
|
||||||
'NULL': 1,
|
|
||||||
'FRIEND_NOT_FOUND': 2,
|
|
||||||
'FRIEND_NOT_CONNECTED': 3,
|
|
||||||
'SENDQ': 4,
|
|
||||||
'TOO_LONG': 5,
|
|
||||||
'EMPTY': 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_FILE_KIND = {
|
|
||||||
'DATA': 0,
|
|
||||||
'AVATAR': 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_FILE_CONTROL = {
|
|
||||||
'RESUME': 0,
|
|
||||||
'PAUSE': 1,
|
|
||||||
'CANCEL': 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FILE_CONTROL = {
|
|
||||||
'OK': 0,
|
|
||||||
'FRIEND_NOT_FOUND': 1,
|
|
||||||
'FRIEND_NOT_CONNECTED': 2,
|
|
||||||
'NOT_FOUND': 3,
|
|
||||||
'NOT_PAUSED': 4,
|
|
||||||
'DENIED': 5,
|
|
||||||
'ALREADY_PAUSED': 6,
|
|
||||||
'SENDQ': 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FILE_SEEK = {
|
|
||||||
'OK': 0,
|
|
||||||
'FRIEND_NOT_FOUND': 1,
|
|
||||||
'FRIEND_NOT_CONNECTED': 2,
|
|
||||||
'NOT_FOUND': 3,
|
|
||||||
'DENIED': 4,
|
|
||||||
'INVALID_POSITION': 5,
|
|
||||||
'SENDQ': 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FILE_GET = {
|
|
||||||
'OK': 0,
|
|
||||||
'NULL': 1,
|
|
||||||
'FRIEND_NOT_FOUND': 2,
|
|
||||||
'NOT_FOUND': 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FILE_SEND = {
|
|
||||||
'OK': 0,
|
|
||||||
'NULL': 1,
|
|
||||||
'FRIEND_NOT_FOUND': 2,
|
|
||||||
'FRIEND_NOT_CONNECTED': 3,
|
|
||||||
'NAME_TOO_LONG': 4,
|
|
||||||
'TOO_MANY': 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FILE_SEND_CHUNK = {
|
|
||||||
'OK': 0,
|
|
||||||
'NULL': 1,
|
|
||||||
'FRIEND_NOT_FOUND': 2,
|
|
||||||
'FRIEND_NOT_CONNECTED': 3,
|
|
||||||
'NOT_FOUND': 4,
|
|
||||||
'NOT_TRANSFERRING': 5,
|
|
||||||
'INVALID_LENGTH': 6,
|
|
||||||
'SENDQ': 7,
|
|
||||||
'WRONG_POSITION': 8,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_FRIEND_CUSTOM_PACKET = {
|
|
||||||
'OK': 0,
|
|
||||||
'NULL': 1,
|
|
||||||
'FRIEND_NOT_FOUND': 2,
|
|
||||||
'FRIEND_NOT_CONNECTED': 3,
|
|
||||||
'INVALID': 4,
|
|
||||||
'EMPTY': 5,
|
|
||||||
'TOO_LONG': 6,
|
|
||||||
'SENDQ': 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_ERR_GET_PORT = {
|
|
||||||
'OK': 0,
|
|
||||||
'NOT_BOUND': 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_CHAT_CHANGE = {
|
|
||||||
'PEER_ADD': 0,
|
|
||||||
'PEER_DEL': 1,
|
|
||||||
'PEER_NAME': 2
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_GROUPCHAT_TYPE = {
|
|
||||||
'TEXT': 0,
|
|
||||||
'AV': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
TOX_PUBLIC_KEY_SIZE = 32
|
|
||||||
|
|
||||||
TOX_ADDRESS_SIZE = TOX_PUBLIC_KEY_SIZE + 6
|
|
||||||
|
|
||||||
TOX_MAX_FRIEND_REQUEST_LENGTH = 1016
|
|
||||||
|
|
||||||
TOX_MAX_MESSAGE_LENGTH = 1372
|
|
||||||
|
|
||||||
TOX_MAX_NAME_LENGTH = 128
|
|
||||||
|
|
||||||
TOX_MAX_STATUS_MESSAGE_LENGTH = 1007
|
|
||||||
|
|
||||||
TOX_SECRET_KEY_SIZE = 32
|
|
||||||
|
|
||||||
TOX_FILE_ID_LENGTH = 32
|
|
||||||
|
|
||||||
TOX_HASH_LENGTH = 32
|
|
||||||
|
|
||||||
TOX_MAX_CUSTOM_PACKET_SIZE = 1373
|
|
|
@ -1,74 +0,0 @@
|
||||||
import libtox
|
|
||||||
from ctypes import c_size_t, create_string_buffer, byref, c_int, ArgumentError, c_char_p, c_bool
|
|
||||||
from toxencryptsave_enums_and_consts import *
|
|
||||||
|
|
||||||
|
|
||||||
class ToxEncryptSave:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.libtoxencryptsave = libtox.LibToxEncryptSave()
|
|
||||||
|
|
||||||
def is_data_encrypted(self, data):
|
|
||||||
"""
|
|
||||||
Checks if given data is encrypted
|
|
||||||
"""
|
|
||||||
func = self.libtoxencryptsave.tox_is_data_encrypted
|
|
||||||
func.restype = c_bool
|
|
||||||
result = func(c_char_p(bytes(data)))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def pass_encrypt(self, data, password):
|
|
||||||
"""
|
|
||||||
Encrypts the given data with the given password.
|
|
||||||
|
|
||||||
: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(bytes(password, 'utf-8')),
|
|
||||||
c_size_t(len(password)),
|
|
||||||
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, password):
|
|
||||||
"""
|
|
||||||
Decrypts the given data with the given password.
|
|
||||||
|
|
||||||
: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(bytes(data)),
|
|
||||||
c_size_t(len(data)),
|
|
||||||
c_char_p(bytes(password, 'utf-8')),
|
|
||||||
c_size_t(len(password)),
|
|
||||||
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.')
|
|
|
@ -1,29 +0,0 @@
|
||||||
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
|
|
|
@ -1,28 +0,0 @@
|
||||||
import util
|
|
||||||
import toxencryptsave
|
|
||||||
|
|
||||||
|
|
||||||
class ToxES(util.Singleton):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self._toxencryptsave = toxencryptsave.ToxEncryptSave()
|
|
||||||
self._passphrase = None
|
|
||||||
|
|
||||||
def set_password(self, passphrase):
|
|
||||||
self._passphrase = passphrase
|
|
||||||
|
|
||||||
def has_password(self):
|
|
||||||
return bool(self._passphrase)
|
|
||||||
|
|
||||||
def is_password(self, password):
|
|
||||||
return self._passphrase == password
|
|
||||||
|
|
||||||
def is_data_encrypted(self, data):
|
|
||||||
return len(data) > 0 and self._toxencryptsave.is_data_encrypted(data)
|
|
||||||
|
|
||||||
def pass_encrypt(self, data):
|
|
||||||
return self._toxencryptsave.pass_encrypt(data, self._passphrase)
|
|
||||||
|
|
||||||
def pass_decrypt(self, data):
|
|
||||||
return self._toxencryptsave.pass_decrypt(data, self._passphrase)
|
|
|
@ -1,110 +0,0 @@
|
||||||
import util
|
|
||||||
import os
|
|
||||||
import settings
|
|
||||||
import platform
|
|
||||||
import urllib
|
|
||||||
from PyQt5 import QtNetwork, QtCore
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
def connection_available():
|
|
||||||
try:
|
|
||||||
urllib.request.urlopen('http://216.58.192.142', timeout=1) # google.com
|
|
||||||
return True
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def updater_available():
|
|
||||||
if is_from_sources():
|
|
||||||
return os.path.exists(util.curr_directory() + '/toxygen_updater.py')
|
|
||||||
elif platform.system() == 'Windows':
|
|
||||||
return os.path.exists(util.curr_directory() + '/toxygen_updater.exe')
|
|
||||||
else:
|
|
||||||
return os.path.exists(util.curr_directory() + '/toxygen_updater')
|
|
||||||
|
|
||||||
|
|
||||||
def check_for_updates():
|
|
||||||
current_version = util.program_version
|
|
||||||
major, minor, patch = list(map(lambda x: int(x), current_version.split('.')))
|
|
||||||
versions = generate_versions(major, minor, patch)
|
|
||||||
for version in versions:
|
|
||||||
if send_request(version):
|
|
||||||
return version
|
|
||||||
return None # no new version was found
|
|
||||||
|
|
||||||
|
|
||||||
def is_from_sources():
|
|
||||||
return __file__.endswith('.py')
|
|
||||||
|
|
||||||
|
|
||||||
def test_url(version):
|
|
||||||
return 'https://github.com/toxygen-project/toxygen/releases/tag/v' + version
|
|
||||||
|
|
||||||
|
|
||||||
def get_url(version):
|
|
||||||
if is_from_sources():
|
|
||||||
return 'https://github.com/toxygen-project/toxygen/archive/v' + version + '.zip'
|
|
||||||
else:
|
|
||||||
if platform.system() == 'Windows':
|
|
||||||
name = 'toxygen_windows.zip'
|
|
||||||
elif util.is_64_bit():
|
|
||||||
name = 'toxygen_linux_64.tar.gz'
|
|
||||||
else:
|
|
||||||
name = 'toxygen_linux.tar.gz'
|
|
||||||
return 'https://github.com/toxygen-project/toxygen/releases/download/v{}/{}'.format(version, name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_params(url, version):
|
|
||||||
if is_from_sources():
|
|
||||||
if platform.system() == 'Windows':
|
|
||||||
return ['python', 'toxygen_updater.py', url, version]
|
|
||||||
else:
|
|
||||||
return ['python3', 'toxygen_updater.py', url, version]
|
|
||||||
elif platform.system() == 'Windows':
|
|
||||||
return [util.curr_directory() + '/toxygen_updater.exe', url, version]
|
|
||||||
else:
|
|
||||||
return ['./toxygen_updater', url, version]
|
|
||||||
|
|
||||||
|
|
||||||
def download(version):
|
|
||||||
os.chdir(util.curr_directory())
|
|
||||||
url = get_url(version)
|
|
||||||
params = get_params(url, version)
|
|
||||||
print('Updating Toxygen')
|
|
||||||
util.log('Updating Toxygen')
|
|
||||||
try:
|
|
||||||
subprocess.Popen(params)
|
|
||||||
except Exception as ex:
|
|
||||||
util.log('Exception: running updater failed with ' + str(ex))
|
|
||||||
|
|
||||||
|
|
||||||
def send_request(version):
|
|
||||||
s = settings.Settings.get_instance()
|
|
||||||
netman = QtNetwork.QNetworkAccessManager()
|
|
||||||
proxy = QtNetwork.QNetworkProxy()
|
|
||||||
if s['proxy_type']:
|
|
||||||
proxy.setType(QtNetwork.QNetworkProxy.Socks5Proxy if s['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy)
|
|
||||||
proxy.setHostName(s['proxy_host'])
|
|
||||||
proxy.setPort(s['proxy_port'])
|
|
||||||
netman.setProxy(proxy)
|
|
||||||
url = test_url(version)
|
|
||||||
try:
|
|
||||||
request = QtNetwork.QNetworkRequest()
|
|
||||||
request.setUrl(QtCore.QUrl(url))
|
|
||||||
reply = netman.get(request)
|
|
||||||
while not reply.isFinished():
|
|
||||||
QtCore.QThread.msleep(1)
|
|
||||||
QtCore.QCoreApplication.processEvents()
|
|
||||||
attr = reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
|
|
||||||
return attr is not None and 200 <= attr < 300
|
|
||||||
except Exception as ex:
|
|
||||||
util.log('TOXYGEN UPDATER ERROR: ' + str(ex))
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def generate_versions(major, minor, patch):
|
|
||||||
new_major = '.'.join([str(major + 1), '0', '0'])
|
|
||||||
new_minor = '.'.join([str(major), str(minor + 1), '0'])
|
|
||||||
new_patch = '.'.join([str(major), str(minor), str(patch + 1)])
|
|
||||||
return new_major, new_minor, new_patch
|
|
107
toxygen/util.py
107
toxygen/util.py
|
@ -1,107 +0,0 @@
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
program_version = '0.4.4'
|
|
||||||
|
|
||||||
|
|
||||||
def cached(func):
|
|
||||||
saved_result = None
|
|
||||||
|
|
||||||
def wrapped_func():
|
|
||||||
nonlocal saved_result
|
|
||||||
if saved_result is None:
|
|
||||||
saved_result = func()
|
|
||||||
|
|
||||||
return saved_result
|
|
||||||
|
|
||||||
return wrapped_func
|
|
||||||
|
|
||||||
|
|
||||||
def log(data):
|
|
||||||
try:
|
|
||||||
with open(curr_directory() + '/logs.log', 'a') as fl:
|
|
||||||
fl.write(str(data) + '\n')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@cached
|
|
||||||
def curr_directory():
|
|
||||||
return os.path.dirname(os.path.realpath(__file__))
|
|
||||||
|
|
||||||
|
|
||||||
def curr_time():
|
|
||||||
return time.strftime('%H:%M')
|
|
||||||
|
|
||||||
|
|
||||||
def copy(src, dest):
|
|
||||||
if not os.path.exists(dest):
|
|
||||||
os.makedirs(dest)
|
|
||||||
src_files = os.listdir(src)
|
|
||||||
for file_name in src_files:
|
|
||||||
full_file_name = os.path.join(src, file_name)
|
|
||||||
if os.path.isfile(full_file_name):
|
|
||||||
shutil.copy(full_file_name, dest)
|
|
||||||
else:
|
|
||||||
copy(full_file_name, os.path.join(dest, file_name))
|
|
||||||
|
|
||||||
|
|
||||||
def remove(folder):
|
|
||||||
if os.path.isdir(folder):
|
|
||||||
shutil.rmtree(folder)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_time(t):
|
|
||||||
offset = time.timezone + time_offset() * 60
|
|
||||||
sec = int(t) - offset
|
|
||||||
m, s = divmod(sec, 60)
|
|
||||||
h, m = divmod(m, 60)
|
|
||||||
d, h = divmod(h, 24)
|
|
||||||
return '%02d:%02d' % (h, m)
|
|
||||||
|
|
||||||
|
|
||||||
@cached
|
|
||||||
def time_offset():
|
|
||||||
hours = int(time.strftime('%H'))
|
|
||||||
minutes = int(time.strftime('%M'))
|
|
||||||
sec = int(time.time()) - time.timezone
|
|
||||||
m, s = divmod(sec, 60)
|
|
||||||
h, m = divmod(m, 60)
|
|
||||||
d, h = divmod(h, 24)
|
|
||||||
result = hours * 60 + minutes - h * 60 - m
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def append_slash(s):
|
|
||||||
if len(s) and s[-1] not in ('\\', '/'):
|
|
||||||
s += '/'
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
@cached
|
|
||||||
def is_64_bit():
|
|
||||||
return sys.maxsize > 2 ** 32
|
|
||||||
|
|
||||||
|
|
||||||
def is_re_valid(regex):
|
|
||||||
try:
|
|
||||||
re.compile(regex)
|
|
||||||
except re.error:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Singleton:
|
|
||||||
_instance = None
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.__class__._instance = self
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_instance(cls):
|
|
||||||
return cls._instance
|
|
|
@ -1,166 +0,0 @@
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
||||||
|
|
||||||
|
|
||||||
class DataLabel(QtWidgets.QLabel):
|
|
||||||
"""
|
|
||||||
Label with elided text
|
|
||||||
"""
|
|
||||||
def setText(self, text):
|
|
||||||
text = ''.join('\u25AF' if len(bytes(c, 'utf-8')) >= 4 else c for c in text)
|
|
||||||
metrics = QtGui.QFontMetrics(self.font())
|
|
||||||
text = metrics.elidedText(text, QtCore.Qt.ElideRight, self.width())
|
|
||||||
super().setText(text)
|
|
||||||
|
|
||||||
|
|
||||||
class ComboBox(QtWidgets.QComboBox):
|
|
||||||
|
|
||||||
def __init__(self, *args):
|
|
||||||
super().__init__(*args)
|
|
||||||
self.view().setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
|
|
||||||
|
|
||||||
|
|
||||||
class CenteredWidget(QtWidgets.QWidget):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(CenteredWidget, self).__init__()
|
|
||||||
self.center()
|
|
||||||
|
|
||||||
def center(self):
|
|
||||||
qr = self.frameGeometry()
|
|
||||||
cp = QtWidgets.QDesktopWidget().availableGeometry().center()
|
|
||||||
qr.moveCenter(cp)
|
|
||||||
self.move(qr.topLeft())
|
|
||||||
|
|
||||||
|
|
||||||
class LineEdit(QtWidgets.QLineEdit):
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super(LineEdit, self).__init__(parent)
|
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
|
||||||
menu = create_menu(self.createStandardContextMenu())
|
|
||||||
menu.exec_(event.globalPos())
|
|
||||||
del menu
|
|
||||||
|
|
||||||
|
|
||||||
class QRightClickButton(QtWidgets.QPushButton):
|
|
||||||
"""
|
|
||||||
Button with right click support
|
|
||||||
"""
|
|
||||||
|
|
||||||
rightClicked = QtCore.pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
super(QRightClickButton, self).__init__(parent)
|
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
|
||||||
if event.button() == QtCore.Qt.RightButton:
|
|
||||||
self.rightClicked.emit()
|
|
||||||
else:
|
|
||||||
super(QRightClickButton, self).mousePressEvent(event)
|
|
||||||
|
|
||||||
|
|
||||||
class RubberBand(QtWidgets.QRubberBand):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(RubberBand, self).__init__(QtWidgets.QRubberBand.Rectangle, None)
|
|
||||||
self.setPalette(QtGui.QPalette(QtCore.Qt.transparent))
|
|
||||||
self.pen = QtGui.QPen(QtCore.Qt.blue, 4)
|
|
||||||
self.pen.setStyle(QtCore.Qt.SolidLine)
|
|
||||||
self.painter = QtGui.QPainter()
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
|
|
||||||
self.painter.begin(self)
|
|
||||||
self.painter.setPen(self.pen)
|
|
||||||
self.painter.drawRect(event.rect())
|
|
||||||
self.painter.end()
|
|
||||||
|
|
||||||
|
|
||||||
class RubberBandWindow(QtWidgets.QWidget):
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
super().__init__()
|
|
||||||
self.parent = parent
|
|
||||||
self.setMouseTracking(True)
|
|
||||||
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
|
|
||||||
self.showFullScreen()
|
|
||||||
self.setWindowOpacity(0.5)
|
|
||||||
self.rubberband = RubberBand()
|
|
||||||
self.rubberband.setWindowFlags(self.rubberband.windowFlags() | QtCore.Qt.FramelessWindowHint)
|
|
||||||
self.rubberband.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
|
||||||
self.origin = event.pos()
|
|
||||||
self.rubberband.setGeometry(QtCore.QRect(self.origin, QtCore.QSize()))
|
|
||||||
self.rubberband.show()
|
|
||||||
QtWidgets.QWidget.mousePressEvent(self, event)
|
|
||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
|
||||||
if self.rubberband.isVisible():
|
|
||||||
self.rubberband.setGeometry(QtCore.QRect(self.origin, event.pos()).normalized())
|
|
||||||
left = QtGui.QRegion(QtCore.QRect(0, 0, self.rubberband.x(), self.height()))
|
|
||||||
right = QtGui.QRegion(QtCore.QRect(self.rubberband.x() + self.rubberband.width(), 0, self.width(), self.height()))
|
|
||||||
top = QtGui.QRegion(0, 0, self.width(), self.rubberband.y())
|
|
||||||
bottom = QtGui.QRegion(0, self.rubberband.y() + self.rubberband.height(), self.width(), self.height())
|
|
||||||
self.setMask(left + right + top + bottom)
|
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
|
||||||
if event.key() == QtCore.Qt.Key_Escape:
|
|
||||||
self.rubberband.setHidden(True)
|
|
||||||
self.close()
|
|
||||||
else:
|
|
||||||
super().keyPressEvent(event)
|
|
||||||
|
|
||||||
|
|
||||||
def create_menu(menu):
|
|
||||||
"""
|
|
||||||
:return translated menu
|
|
||||||
"""
|
|
||||||
for action in menu.actions():
|
|
||||||
text = action.text()
|
|
||||||
if 'Link Location' in text:
|
|
||||||
text = text.replace('Copy &Link Location',
|
|
||||||
QtWidgets.QApplication.translate("MainWindow", "Copy link location"))
|
|
||||||
elif '&Copy' in text:
|
|
||||||
text = text.replace('&Copy', QtWidgets.QApplication.translate("MainWindow", "Copy"))
|
|
||||||
elif 'All' in text:
|
|
||||||
text = text.replace('Select All', QtWidgets.QApplication.translate("MainWindow", "Select all"))
|
|
||||||
elif 'Delete' in text:
|
|
||||||
text = text.replace('Delete', QtWidgets.QApplication.translate("MainWindow", "Delete"))
|
|
||||||
elif '&Paste' in text:
|
|
||||||
text = text.replace('&Paste', QtWidgets.QApplication.translate("MainWindow", "Paste"))
|
|
||||||
elif 'Cu&t' in text:
|
|
||||||
text = text.replace('Cu&t', QtWidgets.QApplication.translate("MainWindow", "Cut"))
|
|
||||||
elif '&Undo' in text:
|
|
||||||
text = text.replace('&Undo', QtWidgets.QApplication.translate("MainWindow", "Undo"))
|
|
||||||
elif '&Redo' in text:
|
|
||||||
text = text.replace('&Redo', QtWidgets.QApplication.translate("MainWindow", "Redo"))
|
|
||||||
else:
|
|
||||||
menu.removeAction(action)
|
|
||||||
continue
|
|
||||||
action.setText(text)
|
|
||||||
return menu
|
|
||||||
|
|
||||||
|
|
||||||
class MultilineEdit(CenteredWidget):
|
|
||||||
|
|
||||||
def __init__(self, title, text, save):
|
|
||||||
super(MultilineEdit, self).__init__()
|
|
||||||
self.resize(350, 200)
|
|
||||||
self.setMinimumSize(QtCore.QSize(350, 200))
|
|
||||||
self.setMaximumSize(QtCore.QSize(350, 200))
|
|
||||||
self.setWindowTitle(title)
|
|
||||||
self.edit = QtWidgets.QTextEdit(self)
|
|
||||||
self.edit.setGeometry(QtCore.QRect(0, 0, 350, 150))
|
|
||||||
self.edit.setText(text)
|
|
||||||
self.button = QtWidgets.QPushButton(self)
|
|
||||||
self.button.setGeometry(QtCore.QRect(0, 150, 350, 50))
|
|
||||||
self.button.setText(QtWidgets.QApplication.translate("MainWindow", "Save"))
|
|
||||||
self.button.clicked.connect(self.button_click)
|
|
||||||
self.center()
|
|
||||||
self.save = save
|
|
||||||
|
|
||||||
def button_click(self):
|
|
||||||
self.save(self.edit.toPlainText())
|
|
||||||
self.close()
|
|
Loading…
Reference in a new issue