diff --git a/src/callbacks.py b/src/callbacks.py index 3b8d960..31f480a 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -4,6 +4,7 @@ from settings import Settings from profile import Profile from toxcore_enums_and_consts import * from tox import bin_to_string +from ctypes import c_char_p, cast, pointer class InvokeEvent(QtCore.QEvent): @@ -28,6 +29,10 @@ _invoker = Invoker() def invoke_in_main_thread(fn, *args, **kwargs): QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - current user +# ----------------------------------------------------------------------------------------------------------------- + def self_connection_status(tox_link): """ @@ -45,6 +50,11 @@ def self_connection_status(tox_link): return wrapped +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - friends +# ----------------------------------------------------------------------------------------------------------------- + + def friend_status(tox, friend_num, new_status, user_data): """ Check friend's status (none, busy, away) @@ -64,6 +74,8 @@ def friend_connection_status(tox, friend_num, new_status, user_data): friend = profile.get_friend_by_number(friend_num) if new_status == TOX_CONNECTION['NONE']: invoke_in_main_thread(friend.set_status, None) + elif friend.status is None: + invoke_in_main_thread(profile.send_avatar, friend_num) invoke_in_main_thread(profile.update_filtration) @@ -116,6 +128,62 @@ def friend_request(tox, public_key, message, message_size, user_data): tox_id = bin_to_string(public_key, TOX_PUBLIC_KEY_SIZE) invoke_in_main_thread(profile.process_friend_request, tox_id, message.decode('utf-8')) +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - file transfers +# ----------------------------------------------------------------------------------------------------------------- + + +def tox_file_recv(window, tray): + 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' + file_name = file_name[:file_name_size] + 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']: + invoke_in_main_thread(tray_notification, 'File from ' + friend.name, file_name, tray) + if settings['sound_notifications']: + sound_notification(SOUND_NOTIFICATION['FILE_TRANSFER']) + 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): + invoke_in_main_thread(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): + Profile.get_instance().outgoing_chunk( + friend_number, + file_number, + position, + size) + + +def file_recv_control(tox, friend_number, file_number, file_control, user_data): + # TODO: process + pass + +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - initialization +# ----------------------------------------------------------------------------------------------------------------- + def init_callbacks(tox, window, tray): """ @@ -124,10 +192,16 @@ def init_callbacks(tox, window, tray): :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_self_connection_status(self_connection_status(tox), 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_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) diff --git a/src/file_transfers.py b/src/file_transfers.py new file mode 100644 index 0000000..91412f2 --- /dev/null +++ b/src/file_transfers.py @@ -0,0 +1,154 @@ +from toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL +from os.path import basename, getsize, exists +from os import remove +from time import time +from tox import Tox +import profile +from PySide import QtCore + + +TOX_FILE_TRANSFER_STATE = { + 'RUNNING': 0, + 'PAUSED': 1, + 'CANCELED': 2, + 'FINISHED': 3, +} + + +class StateSignal(QtCore.QObject): + signal = QtCore.Signal(int, float) + + +class FileTransfer(QtCore.QObject): + + 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 = time() + self._size = float(size) + self._done = 0 + self._state_changed = StateSignal() + + def set_tox(self, tox): + self._tox = tox + + def set_state_changed_handler(self, handler): + self._state_changed.signal.connect(handler) + + def get_file_number(self): + return self._file_number + + def get_friend_number(self): + return self._friend_number + + def cancel(self): + self.send_control(TOX_FILE_CONTROL['CANCEL']) + self._file.close() + self._state_changed.signal.emit(self.state, self._done / self._size) + + def send_control(self, control): + if self._tox.file_control(self._friend_number, self._file_number, control): + self.state = control + self._state_changed.signal.emit(self.state, self._done / self._size if self._size else 0) + + def get_file_id(self): + return self._tox.file_get_file_id(self._friend_number, self._file_number) + + def file_seek(self): + # TODO implement or not implement + pass + + +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._file_number = tox.file_send(friend_number, kind, size, file_id, + basename(path).encode('utf-8') if path else '') + + def send_chunk(self, position, size): + 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 + self._state_changed.signal.emit(self.state, self._done / self._size) + else: + self._file.close() + self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] + self._state_changed.signal.emit(self.state, 1) + + +class SendAvatar(SendTransfer): + + 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 ReceiveTransfer(FileTransfer): + + def __init__(self, path, tox, friend_number, size, file_number): + super(ReceiveTransfer, self).__init__(path, tox, friend_number, size, file_number) + self._file = open(self._path, 'wb') + self._file.truncate(0) + self._file_size = 0 + + def cancel(self): + super(ReceiveTransfer, self).cancel() + remove(self._path) + + def write_chunk(self, position, data): + if data is None: + self._file.close() + self.state = TOX_FILE_TRANSFER_STATE['FINISHED'] + self._state_changed.signal.emit(self.state, self._done / self._size if self._size else 0) + else: + data = ''.join(chr(x) for x in data) + if self._file_size < position: + self._file.seek(0, 2) + self._file.write('\0' * (position - self._file_size)) + self._file.seek(position) + self._file.write(data) + self._file.flush() + l = len(data) + if position + l > self._file_size: + self._file_size = position + l + self._done += l + self._state_changed.signal.emit(self.state, self._done / self._size) + + +class ReceiveAvatar(ReceiveTransfer): + + def __init__(self, tox, friend_number, size, file_number): + path = profile.ProfileHelper.get_path() + '/avatars/{}.png'.format(tox.friend_get_public_key(friend_number)) + super(ReceiveAvatar, self).__init__(path, tox, friend_number, size, file_number) + if exists(path): + if not size: + remove(path) + self.send_control(TOX_FILE_CONTROL['CANCEL']) + self.state = TOX_FILE_TRANSFER_STATE['CANCELED'] + else: + hash = self.get_file_id() + with open(path, 'rb') as fl: + existing_hash = Tox.hash(fl.read()) + if hash == existing_hash: + self.send_control(TOX_FILE_CONTROL['CANCEL']) + self.state = TOX_FILE_TRANSFER_STATE['CANCELED'] + else: + self.send_control(TOX_FILE_CONTROL['RESUME']) + else: + self.send_control(TOX_FILE_CONTROL['RESUME']) diff --git a/src/images/accept.png b/src/images/accept.png new file mode 100755 index 0000000..5612f26 Binary files /dev/null and b/src/images/accept.png differ diff --git a/src/images/decline.png b/src/images/decline.png new file mode 100755 index 0000000..dfdf85d Binary files /dev/null and b/src/images/decline.png differ diff --git a/src/list_items.py b/src/list_items.py index 97dbcad..e96e49b 100644 --- a/src/list_items.py +++ b/src/list_items.py @@ -1,5 +1,8 @@ from toxcore_enums_and_consts import * from PySide import QtGui, QtCore +import profile +from file_transfers import TOX_FILE_TRANSFER_STATE +from util import curr_directory class MessageEdit(QtGui.QPlainTextEdit): @@ -154,3 +157,97 @@ class StatusCircle(QtGui.QWidget): paint.setPen(color) paint.drawEllipse(center, rad_x + 3, rad_y + 3) paint.end() + + +class FileTransferItem(QtGui.QListWidget): + def __init__(self, file_name, size, time, user, friend_number, file_number, show_accept, parent=None): + QtGui.QListWidget.__init__(self, parent) + self.resize(QtCore.QSize(600, 50)) + self.setStyleSheet('QListWidget { background-color: green; }') + + self.name = QtGui.QLabel(self) + self.name.setGeometry(QtCore.QRect(0, 15, 95, 20)) + self.name.setTextFormat(QtCore.Qt.PlainText) + font = QtGui.QFont() + font.setFamily("Times New Roman") + font.setPointSize(11) + font.setBold(True) + self.name.setFont(font) + self.name.setObjectName("name") + self.name.setText(user if len(user) <= 14 else user[:11] + '...') + self.name.setStyleSheet('QLabel { color: black; }') + + self.time = QtGui.QLabel(self) + self.time.setGeometry(QtCore.QRect(550, 0, 50, 50)) + font.setPointSize(10) + font.setBold(False) + self.time.setFont(font) + self.time.setObjectName("time") + self.time.setText(time) + self.time.setStyleSheet('QLabel { color: black; }') + + self.cancel = QtGui.QPushButton(self) + self.cancel.setGeometry(QtCore.QRect(500, 0, 50, 50)) + pixmap = QtGui.QPixmap(curr_directory() + '/images/decline.png') + icon = QtGui.QIcon(pixmap) + self.cancel.setIcon(icon) + self.cancel.setIconSize(QtCore.QSize(50, 50)) + self.cancel.clicked.connect(lambda: self.cancel_transfer(friend_number, file_number)) + + self.accept = QtGui.QPushButton(self) + self.accept.setGeometry(QtCore.QRect(450, 0, 50, 50)) + pixmap = QtGui.QPixmap(curr_directory() + '/images/accept.png') + icon = QtGui.QIcon(pixmap) + self.accept.setIcon(icon) + self.accept.setIconSize(QtCore.QSize(50, 50)) + self.accept.clicked.connect(lambda: self.accept_transfer(friend_number, file_number, size)) + self.accept.setVisible(show_accept) + + self.pb = QtGui.QProgressBar(self) + self.pb.setGeometry(QtCore.QRect(100, 15, 100, 20)) + self.pb.setValue(0) + + self.file_name = QtGui.QLabel(self) + self.file_name.setGeometry(QtCore.QRect(210, 0, 230, 50)) + font.setPointSize(12) + self.file_name.setFont(font) + self.file_name.setObjectName("time") + file_size = size / 1024 + if not file_size: + file_size = '<1KB' + elif file_size >= 1024: + file_size = '{}MB'.format(file_size / 1024) + else: + file_size = '{}KB'.format(file_size) + file_data = u'{} {}'.format(file_size, file_name) + self.file_name.setText(file_data if len(file_data) <= 27 else file_data[:24] + '...') + self.file_name.setStyleSheet('QLabel { color: black; }') + self.saved_name = file_name + + def cancel_transfer(self, friend_number, file_number): + pr = profile.Profile.get_instance() + pr.cancel_transfer(friend_number, file_number) + self.setStyleSheet('QListWidget { background-color: red; }') + self.cancel.setVisible(False) + self.accept.setVisible(False) + self.pb.setVisible(False) + + def accept_transfer(self, friend_number, file_number, size): + directory = QtGui.QFileDialog.getExistingDirectory() + if directory: + pr = profile.Profile.get_instance() + pr.accept_transfer(self, directory + '/' + self.saved_name, friend_number, file_number, size) + self.accept.setVisible(False) + + @QtCore.Slot(int, float) + def update(self, state, progress): + self.pb.setValue(int(progress * 100)) + if state == TOX_FILE_TRANSFER_STATE['CANCELED']: + self.setStyleSheet('QListWidget { background-color: red; }') + self.cancel.setVisible(False) + self.accept.setVisible(False) + self.pb.setVisible(False) + elif state == TOX_FILE_TRANSFER_STATE['FINISHED']: + self.pb.setVisible(False) + self.cancel.setVisible(False) + diff --git a/src/mainscreen.py b/src/mainscreen.py index 27ddb34..7b268ad 100644 --- a/src/mainscreen.py +++ b/src/mainscreen.py @@ -102,6 +102,7 @@ class MainWindow(QtGui.QMainWindow): self.sendMessageButton.setGeometry(QtCore.QRect(550, 10, 60, 110)) self.sendMessageButton.setObjectName("sendMessageButton") self.sendMessageButton.clicked.connect(self.send_message) + pixmap = QtGui.QPixmap(curr_directory() + '/images/send.png') icon = QtGui.QIcon(pixmap) self.sendMessageButton.setIcon(icon) @@ -114,6 +115,9 @@ class MainWindow(QtGui.QMainWindow): icon = QtGui.QIcon(pixmap) self.screenshotButton.setIcon(icon) self.screenshotButton.setIconSize(QtCore.QSize(90, 40)) + + self.fileTransferButton.clicked.connect(self.send_file) + self.screenshotButton.clicked.connect(self.send_screenshot) QtCore.QMetaObject.connectSlotsByName(Form) def setup_left_bottom(self, Form): @@ -277,51 +281,75 @@ class MainWindow(QtGui.QMainWindow): self.int_s.show() # ----------------------------------------------------------------------------------------------------------------- - # Messages + # Messages and file transfers # ----------------------------------------------------------------------------------------------------------------- def send_message(self): text = self.messageEdit.toPlainText() self.profile.send_message(text) + def send_file(self): + if self.profile.is_active_online(): # active friend exists and online + name = QtGui.QFileDialog.getOpenFileName(self, 'Choose file') + if name[0]: + self.profile.send_file(name[0]) + + def send_screenshot(self): + # TODO: add screenshots support + if self.profile.is_active_online(): # active friend exists and online + pass + # ----------------------------------------------------------------------------------------------------------------- # 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_by_number(num) + settings = Settings.get_instance() + allowed = friend.tox_id in settings['auto_accept_from_friends'] + auto = 'Disallow auto accept' if allowed else 'Allow auto accept' if item is not None: self.listMenu = QtGui.QMenu() set_alias_item = self.listMenu.addAction('Set alias') clear_history_item = self.listMenu.addAction('Clear history') copy_key_item = self.listMenu.addAction('Copy public key') + auto_accept_item = self.listMenu.addAction(auto) remove_item = self.listMenu.addAction('Remove friend') - self.connect(set_alias_item, QtCore.SIGNAL("triggered()"), lambda: self.set_alias(item)) - self.connect(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(item)) - self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(item)) - self.connect(clear_history_item, QtCore.SIGNAL("triggered()"), lambda: self.clear_history(item)) + self.connect(set_alias_item, QtCore.SIGNAL("triggered()"), lambda: self.set_alias(num)) + self.connect(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(num)) + self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num)) + self.connect(clear_history_item, QtCore.SIGNAL("triggered()"), lambda: self.clear_history(num)) + self.connect(auto_accept_item, QtCore.SIGNAL("triggered()"), lambda: self.auto_accept(num, not allowed)) parent_position = self.friends_list.mapToGlobal(QtCore.QPoint(0, 0)) self.listMenu.move(parent_position + pos) self.listMenu.show() - def set_alias(self, item): - num = self.friends_list.indexFromItem(item).row() + def set_alias(self, num): self.profile.set_alias(num) - def remove_friend(self, item): - num = self.friends_list.indexFromItem(item).row() + def remove_friend(self, num): self.profile.delete_friend(num) - def copy_friend_key(self, item): - num = self.friends_list.indexFromItem(item).row() + def copy_friend_key(self, num): tox_id = self.profile.friend_public_key(num) clipboard = QtGui.QApplication.clipboard() clipboard.setText(tox_id) - def clear_history(self, item): - num = self.friends_list.indexFromItem(item).row() + def clear_history(self, num): self.profile.clear_history(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: + index = settings['auto_accept_from_friends'].index(tox_id) + del settings['auto_accept_from_friends'][index] + settings.save() + # ----------------------------------------------------------------------------------------------------------------- # Functions which called when user click somewhere else # ----------------------------------------------------------------------------------------------------------------- diff --git a/src/menu.py b/src/menu.py index af1dd3f..d464659 100644 --- a/src/menu.py +++ b/src/menu.py @@ -1,7 +1,7 @@ from PySide import QtCore, QtGui from settings import Settings from profile import Profile, ProfileHelper -from util import get_style +from util import get_style, curr_directory class CenteredWidget(QtGui.QWidget): @@ -165,7 +165,7 @@ class ProfileSettings(CenteredWidget): Profile.get_instance().reset_avatar() def set_avatar(self): - name = QtGui.QFileDialog.getOpenFileName(self, 'Open file') + name = QtGui.QFileDialog.getOpenFileName(self, 'Open file', None, 'Image Files (*.png)') print name if name[0]: with open(name[0], 'rb') as f: @@ -174,11 +174,12 @@ class ProfileSettings(CenteredWidget): def export_profile(self): directory = QtGui.QFileDialog.getExistingDirectory() + '/' - ProfileHelper.export_profile(directory) - settings = Settings.get_instance() - settings.export(directory) - profile = Profile.get_instance() - profile.export_history(directory) + if directory != '/': + ProfileHelper.export_profile(directory) + settings = Settings.get_instance() + settings.export(directory) + profile = Profile.get_instance() + profile.export_history(directory) def closeEvent(self, event): profile = Profile.get_instance() @@ -275,22 +276,30 @@ class PrivacySettings(CenteredWidget): self.fileautoaccept.setGeometry(QtCore.QRect(40, 60, 271, 22)) self.fileautoaccept.setObjectName("fileautoaccept") self.typingNotifications = QtGui.QCheckBox(self) - self.typingNotifications.setGeometry(QtCore.QRect(40, 90, 350, 31)) - self.typingNotifications.setBaseSize(QtCore.QSize(350, 200)) + self.typingNotifications.setGeometry(QtCore.QRect(40, 90, 350, 30)) self.typingNotifications.setObjectName("typingNotifications") - + self.auto_path = QtGui.QLabel(self) + self.auto_path.setGeometry(QtCore.QRect(40, 120, 350, 30)) + self.path = QtGui.QPlainTextEdit(self) + self.path.setGeometry(QtCore.QRect(10, 160, 330, 30)) + self.change_path = QtGui.QPushButton(self) + self.change_path.setGeometry(QtCore.QRect(230, 120, 100, 30)) self.retranslateUi() settings = Settings.get_instance() self.typingNotifications.setChecked(settings['typing_notifications']) self.fileautoaccept.setChecked(settings['allow_auto_accept']) self.saveHistory.setChecked(settings['save_history']) + self.path.setPlainText(settings['auto_accept_path'] or curr_directory()) + self.change_path.clicked.connect(self.new_path) QtCore.QMetaObject.connectSlotsByName(self) def retranslateUi(self): self.setWindowTitle(QtGui.QApplication.translate("privacySettings", "Privacy settings", None, QtGui.QApplication.UnicodeUTF8)) self.saveHistory.setText(QtGui.QApplication.translate("privacySettings", "Save chat history", None, QtGui.QApplication.UnicodeUTF8)) - self.fileautoaccept.setText(QtGui.QApplication.translate("privacySettings", "Allow file autoaccept", None, QtGui.QApplication.UnicodeUTF8)) + self.fileautoaccept.setText(QtGui.QApplication.translate("privacySettings", "Allow file auto accept", None, QtGui.QApplication.UnicodeUTF8)) self.typingNotifications.setText(QtGui.QApplication.translate("privacySettings", "Send typing notifications", None, QtGui.QApplication.UnicodeUTF8)) + self.auto_path.setText(QtGui.QApplication.translate("privacySettings", "Auto accept default path:", None, QtGui.QApplication.UnicodeUTF8)) + self.change_path.setText(QtGui.QApplication.translate("privacySettings", "Change", None, QtGui.QApplication.UnicodeUTF8)) def closeEvent(self, event): settings = Settings.get_instance() @@ -299,8 +308,14 @@ class PrivacySettings(CenteredWidget): if settings['save_history'] and not self.saveHistory.isChecked(): # clear history Profile.get_instance().clear_history() settings['save_history'] = self.saveHistory.isChecked() + settings['auto_accept_path'] = self.path.toPlainText() settings.save() + def new_path(self): + directory = QtGui.QFileDialog.getExistingDirectory() + '/' + if directory != '/': + self.path.setPlainText(directory) + class NotificationsSettings(CenteredWidget): """Notifications settings form""" diff --git a/src/notifications.py b/src/notifications.py index d090afd..b736f0e 100644 --- a/src/notifications.py +++ b/src/notifications.py @@ -2,6 +2,7 @@ from PySide import QtGui from PySide.phonon import Phonon from util import curr_directory # TODO: make app icon active +# TODO: add all sound notifications SOUND_NOTIFICATION = { @@ -19,9 +20,10 @@ def tray_notification(title, text, tray): def sound_notification(t): - # TODO: add other sound notifications if t == SOUND_NOTIFICATION['MESSAGE']: f = curr_directory() + '/sounds/message.wav' + elif t == SOUND_NOTIFICATION['FILE_TRANSFER']: + f = curr_directory() + '/sounds/file.wav' else: return m = Phonon.MediaSource(f) diff --git a/src/profile.py b/src/profile.py index e6606f2..2e727ae 100644 --- a/src/profile.py +++ b/src/profile.py @@ -1,4 +1,4 @@ -from list_items import MessageItem, ContactItem +from list_items import MessageItem, ContactItem, FileTransferItem from PySide import QtCore, QtGui from tox import Tox import os @@ -8,6 +8,7 @@ from ctypes import * from util import curr_time, log, Singleton, curr_directory, convert_time from tox_dns import tox_dns from history import * +from file_transfers import * import time @@ -56,7 +57,7 @@ class ProfileHelper(object): @staticmethod def export_profile(new_path): - new_path += ProfileHelper._path.split('/')[-1] + new_path += os.path.basename(ProfileHelper._path) with open(ProfileHelper._path, 'rb') as fin: data = fin.read() with open(new_path, 'wb') as fout: @@ -161,7 +162,7 @@ class Contact(object): """ Tries to load avatar of contact or uses default avatar """ - avatar_path = (Settings.get_default_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) + avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) if not os.path.isfile(avatar_path): # load default image avatar_path = curr_directory() + '/images/avatar.png' pixmap = QtGui.QPixmap(QtCore.QSize(64, 64)) @@ -170,17 +171,25 @@ class Contact(object): self._widget.avatar_label.repaint() def reset_avatar(self): - avatar_path = (Settings.get_default_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) + 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 = (Settings.get_default_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) + 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_avatar_hash(self): + # avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) + # if not os.path.isfile(avatar_path): # load default image + # return 0 + # with open(avatar_path, 'rb') as fl: + # data = fl.read() + # return Tox.hash(data) + class Friend(Contact): """ @@ -318,6 +327,7 @@ class Profile(Contact, Singleton): self._screen = screen self._messages = screen.messages self._tox = tox + self._file_transfers = {} # dict of file transfers. key - tuple (friend_number, file_number) settings = Settings.get_instance() self._show_online = settings['show_online_friends'] screen.online_contacts.setChecked(self._show_online) @@ -497,7 +507,7 @@ class Profile(Contact, Singleton): self._messages.scrollToBottom() self._friends[self._active_friend].append_message((message.decode('utf-8'), MESSAGE_OWNER['FRIEND'], - time.time(), + int(time.time()), message_type)) else: friend = filter(lambda x: x.number == friend_num, self._friends)[0] @@ -537,11 +547,12 @@ class Profile(Contact, Singleton): Save history to db """ print 'In save' - if Settings.get_instance()['save_history']: - for friend in self._friends: - messages = friend.get_corr_for_saving() - self._history.save_messages_to_db(friend.tox_id, messages) - del self._history + if hasattr(self, '_history'): + if Settings.get_instance()['save_history']: + for friend in self._friends: + messages = friend.get_corr_for_saving() + self._history.save_messages_to_db(friend.tox_id, messages) + del self._history def clear_history(self, num=None): if num is not None: @@ -560,7 +571,7 @@ class Profile(Contact, Singleton): self._history.export(directory) # ----------------------------------------------------------------------------------------------------------------- - # Factories for friend and message items + # Factories for friend, message and file transfer items # ----------------------------------------------------------------------------------------------------------------- def create_friend_item(self): @@ -578,11 +589,21 @@ class Profile(Contact, Singleton): def create_message_item(self, text, time, name, message_type): item = MessageItem(text, time, name, message_type, self._messages) elem = QtGui.QListWidgetItem(self._messages) - elem.setSizeHint(QtCore.QSize(500, item.getHeight())) + elem.setSizeHint(QtCore.QSize(600, item.getHeight())) self._messages.addItem(elem) self._messages.setItemWidget(elem, item) self._messages.repaint() + def create_file_transfer_item(self, file_name, size, friend_number, file_number, show_accept): + friend = self.get_friend_by_number(friend_number) + item = FileTransferItem(file_name, size, curr_time(), friend.name, friend_number, file_number, show_accept) + elem = QtGui.QListWidgetItem(self._messages) + elem.setSizeHint(QtCore.QSize(600, 50)) + self._messages.addItem(elem) + self._messages.setItemWidget(elem, item) + self._messages.repaint() + return item + # ----------------------------------------------------------------------------------------------------------------- # Work with friends (remove, set alias, get public key) # ----------------------------------------------------------------------------------------------------------------- @@ -701,6 +722,86 @@ class Profile(Contact, Singleton): self.status = None for friend in self._friends: friend.status = None + # TODO: FT reset + + # ----------------------------------------------------------------------------------------------------------------- + # File transfers support + # ----------------------------------------------------------------------------------------------------------------- + + def incoming_file_transfer(self, friend_number, file_number, size, file_name): + settings = Settings.get_instance() + friend = self.get_friend_by_number(friend_number) + if settings['allow_auto_accept'] and friend.tox_id in settings['auto_accept_from_friends']: + path = settings['auto_accept_path'] or curr_directory() + self.accept_transfer(path + '/' + file_name.decode('utf-8'), friend_number, file_number) + self.create_file_transfer_item(file_name.decode('utf-8'), size, friend_number, file_number, False) + else: + self.create_file_transfer_item(file_name.decode('utf-8'), size, friend_number, file_number, True) + + def cancel_transfer(self, friend_number, file_number): + if (friend_number, file_number) in self._file_transfers: + tr = self._file_transfers[(friend_number, file_number)] + tr.cancel() + del self._file_transfers[(friend_number, file_number)] + + def accept_transfer(self, item, path, friend_number, file_number, size): + rt = ReceiveTransfer(path, self._tox, friend_number, size, file_number) + self._file_transfers[(friend_number, file_number)] = rt + self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['RESUME']) + rt.set_state_changed_handler(item.update) + + def incoming_avatar(self, friend_number, file_number, size): + """ + Friend changed avatar + :param friend_number: friend number + :param file_number: file number + :param size: size of avatar or 0 (default avatar) + """ + ra = ReceiveAvatar(self._tox, friend_number, size, file_number) + if ra.state != TOX_FILE_TRANSFER_STATE['CANCELED']: + self._file_transfers[(friend_number, file_number)] = ra + else: + self.get_friend_by_number(friend_number).load_avatar() + + def incoming_chunk(self, friend_number, file_number, position, data): + if (friend_number, file_number) in self._file_transfers: + transfer = self._file_transfers[(friend_number, file_number)] + transfer.write_chunk(position, data) + if transfer.state: + if type(transfer) is ReceiveAvatar: + self.get_friend_by_number(friend_number).load_avatar() + del self._file_transfers[(friend_number, file_number)] + + def send_avatar(self, friend_number): + avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) + if not os.path.isfile(avatar_path): # reset image + avatar_path = None + sa = SendAvatar(avatar_path, self._tox, friend_number) + self._file_transfers[(friend_number, sa.get_file_number())] = sa + + def send_file(self, path): + friend_number = self.get_active_number() + st = SendTransfer(path, self._tox, friend_number) + self._file_transfers[(friend_number, st.get_file_number())] = st + item = self.create_file_transfer_item(os.path.basename(path), os.path.getsize(path), friend_number, st.get_file_number(), False) + st.set_state_changed_handler(item.update) + + def outgoing_chunk(self, friend_number, file_number, position, size): + if (friend_number, file_number) in self._file_transfers: + transfer = self._file_transfers[(friend_number, file_number)] + transfer.send_chunk(position, size) + if transfer.state: + del self._file_transfers[(friend_number, file_number)] + + def reset_avatar(self): + super(Profile, self).reset_avatar() + for friend in filter(lambda x: x.status is not None, self._friends): + self.send_avatar(friend.number) + + def set_avatar(self, data): + super(Profile, self).set_avatar(data) + for friend in filter(lambda x: x.status is not None, self._friends): + self.send_avatar(friend.number) def tox_factory(data=None, settings=None): diff --git a/src/settings.py b/src/settings.py index 651b692..9308e08 100644 --- a/src/settings.py +++ b/src/settings.py @@ -53,6 +53,7 @@ class Settings(Singleton, dict): 'save_history': False, 'allow_inline': True, 'allow_auto_accept': False, + 'auto_accept_path': None, 'show_online_friends': False, 'auto_accept_from_friends': [], 'friends_aliases': [], diff --git a/src/sounds/file.wav b/src/sounds/file.wav new file mode 100644 index 0000000..ea86e2e Binary files /dev/null and b/src/sounds/file.wav differ diff --git a/src/sounds/message.wav b/src/sounds/message.wav new file mode 100644 index 0000000..8b75224 Binary files /dev/null and b/src/sounds/message.wav differ diff --git a/src/styles/style.qss b/src/styles/style.qss index 011dbe7..8f9a577 100644 --- a/src/styles/style.qss +++ b/src/styles/style.qss @@ -1239,4 +1239,9 @@ MessageItem::focus MessageEdit:hover { border: none; +} + +QListWidget QPushButton +{ + background-color: transparent; } \ No newline at end of file diff --git a/src/tox.py b/src/tox.py index 3ec7bed..b1fb202 100644 --- a/src/tox.py +++ b/src/tox.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from ctypes import c_char_p, Structure, CDLL, c_bool, addressof, c_int, c_size_t, POINTER, c_uint16, c_void_p, c_uint64 -from ctypes import create_string_buffer, ArgumentError, CFUNCTYPE, c_uint32, sizeof +from ctypes import create_string_buffer, ArgumentError, CFUNCTYPE, c_uint32, sizeof, c_uint8 from platform import system from toxcore_enums_and_consts import * @@ -36,7 +36,7 @@ class LibToxCore(object): def string_to_bin(tox_id): - return c_char_p(tox_id.decode('hex')) + return c_char_p(tox_id.decode('hex')) if tox_id is not None else None def bin_to_string(raw_id, length): @@ -1351,7 +1351,7 @@ class Tox(object): 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_uint64, c_char_p, c_size_t, c_void_p) + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint64, POINTER(c_uint8), c_size_t, c_void_p) self.file_recv_chunk_cb = c_callback(callback) self.libtoxcore.tox_callback_file_recv_chunk(self._tox_pointer, self.file_recv_chunk_cb, user_data) diff --git a/src/util.py b/src/util.py index 3e9cb4c..9c02b38 100644 --- a/src/util.py +++ b/src/util.py @@ -3,7 +3,7 @@ import time from platform import system -program_version = '0.0.1 (alpha)' +program_version = '0.0.2 (alpha)' def log(data):