Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
f4d806f5fc | |||
4854b6151d | |||
c755b4a52a | |||
7505b06ddf | |||
ace663804e | |||
2ff41313f8 | |||
1e1772e306 | |||
300b28bdfa | |||
1f4e81af35 | |||
335d646c42 | |||
b6f5123495 |
57
README.md
57
README.md
@ -14,35 +14,34 @@ Toxygen is powerful cross-platform [Tox](https://tox.chat/) client written in pu
|
||||
|
||||
### Features:
|
||||
|
||||
- [x] 1v1 messages
|
||||
- [x] File transfers
|
||||
- [x] Audio calls
|
||||
- [x] Video calls
|
||||
- [x] Plugins support
|
||||
- [x] Chat history
|
||||
- [x] Emoticons
|
||||
- [x] Stickers
|
||||
- [x] Screenshots
|
||||
- [x] Name lookups (toxme.io support)
|
||||
- [x] Save file encryption
|
||||
- [x] Profile import and export
|
||||
- [x] Faux offline messaging
|
||||
- [x] Faux offline file transfers
|
||||
- [x] Inline images
|
||||
- [x] Message splitting
|
||||
- [x] Proxy support
|
||||
- [x] Avatars
|
||||
- [x] Multiprofile
|
||||
- [x] Multilingual
|
||||
- [x] Sound notifications
|
||||
- [x] Contact aliases
|
||||
- [x] Contact blocking
|
||||
- [x] Typing notifications
|
||||
- [x] Changing nospam
|
||||
- [x] File resuming
|
||||
- [x] Read receipts
|
||||
- [ ] Desktop sharing
|
||||
- [ ] Group chats
|
||||
- 1v1 messages
|
||||
- File transfers
|
||||
- Audio calls
|
||||
- Video calls
|
||||
- Plugins support
|
||||
- Desktop sharing
|
||||
- Chat history
|
||||
- Emoticons
|
||||
- Stickers
|
||||
- Screenshots
|
||||
- Name lookups (toxme.io support)
|
||||
- Save file encryption
|
||||
- Profile import and export
|
||||
- Faux offline messaging
|
||||
- Faux offline file transfers
|
||||
- Inline images
|
||||
- Message splitting
|
||||
- Proxy support
|
||||
- Avatars
|
||||
- Multiprofile
|
||||
- Multilingual
|
||||
- Sound notifications
|
||||
- Contact aliases
|
||||
- Contact blocking
|
||||
- Typing notifications
|
||||
- Changing nospam
|
||||
- File resuming
|
||||
- Read receipts
|
||||
|
||||
### Downloads
|
||||
[Releases](https://github.com/toxygen-project/toxygen/releases)
|
||||
|
@ -17,7 +17,7 @@ Run app using ``toxygen`` command.
|
||||
2. Install PortAudio:
|
||||
``sudo apt-get install portaudio19-dev``
|
||||
3. For 32-bit Linux install PyQt5: ``sudo apt-get install python3-pyqt5``
|
||||
4. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html)
|
||||
4. Install [OpenCV](http://docs.opencv.org/trunk/d7/d9f/tutorial_linux_install.html) or via ``sudo apt-get install python3-opencv``
|
||||
5. Install toxygen:
|
||||
``sudo pip3 install toxygen``
|
||||
6. Run toxygen using ``toxygen`` command.
|
||||
@ -38,10 +38,10 @@ Note: 32-bit Python isn't supported due to bug with videocalls. It is strictly r
|
||||
2. Install PyQt5: ``pip install pyqt5``
|
||||
3. Install PyAudio: ``pip install pyaudio``
|
||||
4. Install numpy: ``pip install numpy``
|
||||
5. Install OpenCV: ``pip install opencv-python`` or via ``sudo apt-get install python3-opencv``
|
||||
5. Install OpenCV: ``pip install opencv-python``
|
||||
6. [Download toxygen](https://github.com/toxygen-project/toxygen/archive/master.zip)
|
||||
7. Unpack archive
|
||||
8. Download latest libtox.dll build, download latest libsodium.a build, put it into \src\libs\
|
||||
8. Download latest libtox.dll build, download latest libsodium.a build, put it into \toxygen\libs\
|
||||
9. Run \toxygen\main.py.
|
||||
|
||||
Optional: install toxygen using setup.py: ``python setup.py install``
|
||||
|
17
setup.py
17
setup.py
@ -8,10 +8,23 @@ import sys
|
||||
|
||||
version = program_version + '.0'
|
||||
|
||||
MODULES = ['PyQt5', 'PyAudio', 'numpy']
|
||||
|
||||
if system() == 'Windows':
|
||||
MODULES.append('opencv-python')
|
||||
MODULES = ['PyQt5', 'PyAudio', 'numpy', 'opencv-python']
|
||||
else:
|
||||
MODULES = []
|
||||
try:
|
||||
import pyaudio
|
||||
except ImportError:
|
||||
MODULES.append('PyAudio')
|
||||
try:
|
||||
import PyQt5
|
||||
except ImportError:
|
||||
MODULES.append('PyQt5')
|
||||
try:
|
||||
import numpy
|
||||
except ImportError:
|
||||
MODULES.append('numpy')
|
||||
|
||||
|
||||
class InstallScript(install):
|
||||
|
@ -6,6 +6,7 @@ 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
|
||||
|
||||
|
||||
@ -203,6 +204,10 @@ class AV:
|
||||
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)
|
||||
|
@ -1,5 +1,5 @@
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from widgets import RubberBand, create_menu, QRightClickButton, CenteredWidget, LineEdit
|
||||
from widgets import RubberBandWindow, create_menu, QRightClickButton, CenteredWidget, LineEdit
|
||||
from profile import Profile
|
||||
import smileys
|
||||
import util
|
||||
@ -71,38 +71,12 @@ class MessageArea(QtWidgets.QPlainTextEdit):
|
||||
self.insertPlainText(text)
|
||||
|
||||
|
||||
class ScreenShotWindow(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ScreenShotWindow, self).__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)
|
||||
class ScreenShotWindow(RubberBandWindow):
|
||||
|
||||
def closeEvent(self, *args):
|
||||
if self.parent.isHidden():
|
||||
self.parent.show()
|
||||
|
||||
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 mouseReleaseEvent(self, event):
|
||||
if self.rubberband.isVisible():
|
||||
self.rubberband.hide()
|
||||
@ -121,13 +95,6 @@ class ScreenShotWindow(QtWidgets.QWidget):
|
||||
Profile.get_instance().send_screenshot(bytes(byte_array.data()))
|
||||
self.close()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == QtCore.Qt.Key_Escape:
|
||||
self.rubberband.setHidden(True)
|
||||
self.close()
|
||||
else:
|
||||
super(ScreenShotWindow, self).keyPressEvent(event)
|
||||
|
||||
|
||||
class SmileyWindow(QtWidgets.QWidget):
|
||||
"""
|
||||
|
@ -2,7 +2,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from settings import *
|
||||
from profile import Profile
|
||||
from util import curr_directory, copy
|
||||
from widgets import CenteredWidget, DataLabel, LineEdit
|
||||
from widgets import CenteredWidget, DataLabel, LineEdit, RubberBandWindow
|
||||
import pyaudio
|
||||
import toxes
|
||||
import plugin_support
|
||||
@ -248,11 +248,11 @@ class ProfileSettings(CenteredWidget):
|
||||
def set_avatar(self):
|
||||
choose = QtWidgets.QApplication.translate("ProfileSettingsForm", "Choose avatar")
|
||||
name = QtWidgets.QFileDialog.getOpenFileName(self, choose, None, 'Images (*.png)',
|
||||
QtGui.QComboBoxQtWidgets.QFileDialog.DontUseNativeDialog)
|
||||
options=QtWidgets.QFileDialog.DontUseNativeDialog)
|
||||
if name[0]:
|
||||
bitmap = QtGui.QPixmap(name[0])
|
||||
bitmap.scaled(QtCore.QSize(128, 128), aspectMode=QtCore.Qt.KeepAspectRatio,
|
||||
mode=QtCore.Qt.SmoothTransformation)
|
||||
bitmap.scaled(QtCore.QSize(128, 128), aspectRatioMode=QtCore.Qt.KeepAspectRatio,
|
||||
transformMode=QtCore.Qt.SmoothTransformation)
|
||||
|
||||
byte_array = QtCore.QByteArray()
|
||||
buffer = QtCore.QBuffer(byte_array)
|
||||
@ -261,8 +261,8 @@ class ProfileSettings(CenteredWidget):
|
||||
Profile.get_instance().set_avatar(bytes(byte_array.data()))
|
||||
|
||||
def export_profile(self):
|
||||
directory = QtWidgets.QFileDialog.getExistingDirectory(options=QtWidgets.QFileDialog.DontUseNativeDialog,
|
||||
dir=curr_directory()) + '/'
|
||||
directory = QtWidgets.QFileDialog.getExistingDirectory(self, '', curr_directory(),
|
||||
QtWidgets.QFileDialog.DontUseNativeDialog) + '/'
|
||||
if directory != '/':
|
||||
reply = QtWidgets.QMessageBox.question(None,
|
||||
QtWidgets.QApplication.translate("ProfileSettingsForm",
|
||||
@ -802,6 +802,18 @@ class AudioSettings(CenteredWidget):
|
||||
settings.save()
|
||||
|
||||
|
||||
class DesktopAreaSelectionWindow(RubberBandWindow):
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.rubberband.isVisible():
|
||||
self.rubberband.hide()
|
||||
rect = self.rubberband.geometry()
|
||||
width, height = rect.width(), rect.height()
|
||||
if width >= 8 and height >= 8:
|
||||
self.parent.save(rect.x(), rect.y(), width, height)
|
||||
self.close()
|
||||
|
||||
|
||||
class VideoSettings(CenteredWidget):
|
||||
"""
|
||||
Audio calls settings form
|
||||
@ -812,6 +824,7 @@ class VideoSettings(CenteredWidget):
|
||||
self.initUI()
|
||||
self.retranslateUi()
|
||||
self.center()
|
||||
self.desktopAreaSelection = None
|
||||
|
||||
def initUI(self):
|
||||
self.setObjectName("videoSettingsForm")
|
||||
@ -831,9 +844,16 @@ class VideoSettings(CenteredWidget):
|
||||
self.input = QtWidgets.QComboBox(self)
|
||||
self.input.setGeometry(QtCore.QRect(25, 30, 350, 30))
|
||||
self.input.currentIndexChanged.connect(self.selectionChanged)
|
||||
self.button = QtWidgets.QPushButton(self)
|
||||
self.button.clicked.connect(self.button_clicked)
|
||||
self.button.setGeometry(QtCore.QRect(25, 70, 350, 30))
|
||||
import cv2
|
||||
self.devices = []
|
||||
self.frame_max_sizes = []
|
||||
self.devices = [-1]
|
||||
screen = QtWidgets.QApplication.primaryScreen()
|
||||
size = screen.size()
|
||||
self.frame_max_sizes = [(size.width(), size.height())]
|
||||
desktop = QtWidgets.QApplication.translate("videoSettingsForm", "Desktop")
|
||||
self.input.addItem(desktop)
|
||||
for i in range(10):
|
||||
v = cv2.VideoCapture(i)
|
||||
if v.isOpened():
|
||||
@ -855,6 +875,10 @@ class VideoSettings(CenteredWidget):
|
||||
def retranslateUi(self):
|
||||
self.setWindowTitle(QtWidgets.QApplication.translate("videoSettingsForm", "Video settings"))
|
||||
self.in_label.setText(QtWidgets.QApplication.translate("videoSettingsForm", "Device:"))
|
||||
self.button.setText(QtWidgets.QApplication.translate("videoSettingsForm", "Select region"))
|
||||
|
||||
def button_clicked(self):
|
||||
self.desktopAreaSelection = DesktopAreaSelectionWindow(self)
|
||||
|
||||
def closeEvent(self, event):
|
||||
try:
|
||||
@ -867,7 +891,23 @@ class VideoSettings(CenteredWidget):
|
||||
except Exception as ex:
|
||||
print('Saving video settings error: ' + str(ex))
|
||||
|
||||
def save(self, x, y, width, height):
|
||||
self.desktopAreaSelection = None
|
||||
settings = Settings.get_instance()
|
||||
settings.video['device'] = -1
|
||||
settings.video['width'] = width
|
||||
settings.video['height'] = height
|
||||
settings.video['x'] = x
|
||||
settings.video['y'] = y
|
||||
settings.save()
|
||||
|
||||
def selectionChanged(self):
|
||||
if self.input.currentIndex() == 0:
|
||||
self.button.setVisible(True)
|
||||
self.video_size.setVisible(False)
|
||||
else:
|
||||
self.button.setVisible(False)
|
||||
self.video_size.setVisible(True)
|
||||
width, height = self.frame_max_sizes[self.input.currentIndex()]
|
||||
self.video_size.clear()
|
||||
dims = [
|
||||
|
@ -667,7 +667,7 @@ class Profile(basecontact.BaseContact, Singleton):
|
||||
dialog = dialog.format(name)
|
||||
title = QtWidgets.QApplication.translate('MainWindow',
|
||||
'Set alias')
|
||||
text, ok = QtGui.QInputDialog.getText(None,
|
||||
text, ok = QtWidgets.QInputDialog.getText(None,
|
||||
title,
|
||||
dialog,
|
||||
QtWidgets.QLineEdit.Normal,
|
||||
|
22
toxygen/screen_sharing.py
Normal file
22
toxygen/screen_sharing.py
Normal file
@ -0,0 +1,22 @@
|
||||
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
|
@ -47,7 +47,7 @@ class Settings(dict, Singleton):
|
||||
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': 0, 'width': 640, 'height': 480}
|
||||
self.video = {'device': -1, 'width': 640, 'height': 480, 'x': 0, 'y': 0}
|
||||
|
||||
@staticmethod
|
||||
def get_auto_profile():
|
||||
@ -57,7 +57,10 @@ class Settings(dict, Singleton):
|
||||
data = fl.read()
|
||||
auto = json.loads(data)
|
||||
if 'path' in auto and 'name' in auto:
|
||||
return str(auto['path']), str(auto['name'])
|
||||
path = str(auto['path'])
|
||||
name = str(auto['name'])
|
||||
if os.path.isfile(append_slash(path) + name + '.tox'):
|
||||
return path, name
|
||||
return '', ''
|
||||
|
||||
@staticmethod
|
||||
|
@ -2,3 +2,28 @@
|
||||
{
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
MessageEdit
|
||||
{
|
||||
border: none;
|
||||
}
|
||||
|
||||
MessageEdit::focus
|
||||
{
|
||||
border: none;
|
||||
}
|
||||
|
||||
MessageItem::focus
|
||||
{
|
||||
border: none;
|
||||
}
|
||||
|
||||
MessageEdit:hover
|
||||
{
|
||||
border: none;
|
||||
}
|
||||
|
||||
MessageEdit
|
||||
{
|
||||
background-color: transparent;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@ import sys
|
||||
import re
|
||||
|
||||
|
||||
program_version = '0.3.0'
|
||||
program_version = '0.3.1'
|
||||
|
||||
|
||||
def cached(func):
|
||||
|
@ -77,6 +77,42 @@ class RubberBand(QtWidgets.QRubberBand):
|
||||
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
|
||||
|
Reference in New Issue
Block a user