From fda07698db7635e08da9259ce9d4b3d5ac3c1ffe Mon Sep 17 00:00:00 2001 From: emdee Date: Tue, 27 Sep 2022 12:36:20 +0000 Subject: [PATCH] merge in next_gen branch --- toxygen/app.py | 424 +++++++++++++++++++++++++++++++++++ toxygen/smileys/__init__.py | 0 toxygen/smileys/smileys.py | 74 ++++++ toxygen/stickers/__init__.py | 0 toxygen/stickers/stickers.py | 18 ++ 5 files changed, 516 insertions(+) create mode 100644 toxygen/app.py create mode 100644 toxygen/smileys/__init__.py create mode 100644 toxygen/smileys/smileys.py create mode 100644 toxygen/stickers/__init__.py create mode 100644 toxygen/stickers/stickers.py diff --git a/toxygen/app.py b/toxygen/app.py new file mode 100644 index 0000000..a23816d --- /dev/null +++ b/toxygen/app.py @@ -0,0 +1,424 @@ +from middleware import threads +import middleware.callbacks as callbacks +from PyQt5 import QtWidgets, QtGui, QtCore +import ui.password_screen as password_screen +import updater.updater as updater +import os +from middleware.tox_factory import tox_factory +import wrapper.toxencryptsave as tox_encrypt_save +import user_data.toxes +from user_data.settings import Settings +from ui.login_screen import LoginScreen +from user_data.profile_manager import ProfileManager +from plugin_support.plugin_support import PluginLoader +from ui.main_screen import MainWindow +from ui import tray +import utils.ui as util_ui +import utils.util as util +from contacts.profile import Profile +from file_transfers.file_transfers_handler import FileTransfersHandler +from contacts.contact_provider import ContactProvider +from contacts.friend_factory import FriendFactory +from contacts.group_factory import GroupFactory +from contacts.contacts_manager import ContactsManager +from av.calls_manager import CallsManager +from history.database import Database +from ui.widgets_factory import WidgetsFactory +from smileys.smileys import SmileyLoader +from ui.items_factories import MessagesItemsFactory, ContactItemsFactory +from messenger.messenger import Messenger +from network.tox_dns import ToxDns +from history.history import History +from file_transfers.file_transfers_messages_service import FileTransfersMessagesService +from groups.groups_service import GroupsService +from ui.create_profile_screen import CreateProfileScreen +from common.provider import Provider +from contacts.group_peer_factory import GroupPeerFactory +from user_data.backup_service import BackupService +import styles.style # TODO: dynamic loading + + +class App: + + def __init__(self, version, path_to_profile=None, uri=None): + self._version = version + self._app = self._settings = self._profile_manager = self._plugin_loader = self._messenger = None + self._tox = self._ms = self._init = self._main_loop = self._av_loop = None + self._uri = self._toxes = self._tray = self._file_transfer_handler = self._contacts_provider = None + self._friend_factory = self._calls_manager = self._contacts_manager = self._smiley_loader = None + self._group_peer_factory = self._tox_dns = self._backup_service = None + self._group_factory = self._groups_service = self._profile = None + if uri is not None and uri.startswith('tox:'): + self._uri = uri[4:] + self._path = path_to_profile + + # ----------------------------------------------------------------------------------------------------------------- + # Public methods + # ----------------------------------------------------------------------------------------------------------------- + + def main(self): + """ + Main function of app. loads login screen if needed and starts main screen + """ + self._app = QtWidgets.QApplication([]) + self._load_icon() + + if util.get_platform() == 'Linux': + QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) + + self._load_base_style() + + if not self._select_and_load_profile(): + return + + if self._try_to_update(): + return + + self._load_app_styles() + self._load_app_translations() + + self._create_dependencies() + self._start_threads() + + if self._uri is not None: + self._ms.add_contact(self._uri) + + self._app.lastWindowClosed.connect(self._app.quit) + + self._execute_app() + + self._stop_app() + + # ----------------------------------------------------------------------------------------------------------------- + # App executing + # ----------------------------------------------------------------------------------------------------------------- + + def _execute_app(self): + while True: + try: + self._app.exec_() + except Exception as ex: + util.log('Unhandled exception: ' + str(ex)) + else: + break + + def _stop_app(self): + self._plugin_loader.stop() + self._stop_threads() + self._file_transfer_handler.stop() + self._tray.hide() + self._save_profile() + self._settings.close() + self._kill_toxav() + self._kill_tox() + + # ----------------------------------------------------------------------------------------------------------------- + # App loading + # ----------------------------------------------------------------------------------------------------------------- + + def _load_base_style(self): + with open(util.join_path(util.get_styles_directory(), 'dark_style.qss')) as fl: + style = fl.read() + self._app.setStyleSheet(style) + + def _load_app_styles(self): + # application color scheme + if self._settings['theme'] == 'dark': + return + for theme in self._settings.built_in_themes().keys(): + if self._settings['theme'] != theme: + continue + theme_path = self._settings.built_in_themes()[theme] + file_path = util.join_path(util.get_styles_directory(), theme_path) + with open(file_path) as fl: + style = fl.read() + self._app.setStyleSheet(style) + break + + def _load_login_screen_translations(self): + current_language, supported_languages = self._get_languages() + if current_language not in supported_languages: + return + lang_path = supported_languages[current_language] + translator = QtCore.QTranslator() + translator.load(util.get_translations_directory() + lang_path) + self._app.installTranslator(translator) + self._app.translator = translator + + def _load_icon(self): + icon_file = os.path.join(util.get_images_directory(), 'icon.png') + self._app.setWindowIcon(QtGui.QIcon(icon_file)) + + @staticmethod + def _get_languages(): + current_locale = QtCore.QLocale() + curr_language = current_locale.languageToString(current_locale.language()) + supported_languages = Settings.supported_languages() + + return curr_language, supported_languages + + def _load_app_translations(self): + lang = Settings.supported_languages()[self._settings['language']] + translator = QtCore.QTranslator() + translator.load(os.path.join(util.get_translations_directory(), lang)) + self._app.installTranslator(translator) + self._app.translator = translator + + def _select_and_load_profile(self): + encrypt_save = tox_encrypt_save.ToxEncryptSave() + self._toxes = user_data.toxes.ToxES(encrypt_save) + + if self._path is not None: # toxygen was started with path to profile + self._load_existing_profile(self._path) + else: + auto_profile = Settings.get_auto_profile() + if auto_profile is None: # no default profile + result = self._select_profile() + if result is None: + return False + if result.is_new_profile(): # create new profile + if not self._create_new_profile(result.profile_path): + return False + else: # load existing profile + self._load_existing_profile(result.profile_path) + self._path = result.profile_path + else: # default profile + self._path = auto_profile + self._load_existing_profile(auto_profile) + + if Settings.is_active_profile(self._path): # profile is in use + profile_name = util.get_profile_name_from_path(self._path) + title = util_ui.tr('Profile {}').format(profile_name) + text = util_ui.tr( + 'Other instance of Toxygen uses this profile or profile was not properly closed. Continue?') + reply = util_ui.question(text, title) + if not reply: + return False + + self._settings.set_active_profile() + + return True + + # ----------------------------------------------------------------------------------------------------------------- + # Threads + # ----------------------------------------------------------------------------------------------------------------- + + def _start_threads(self, initial_start=True): + # init thread + self._init = threads.InitThread(self._tox, self._plugin_loader, self._settings, initial_start) + self._init.start() + + # starting threads for tox iterate and toxav iterate + self._main_loop = threads.ToxIterateThread(self._tox) + self._main_loop.start() + self._av_loop = threads.ToxAVIterateThread(self._tox.AV) + self._av_loop.start() + + if initial_start: + threads.start_file_transfer_thread() + + def _stop_threads(self, is_app_closing=True): + self._init.stop_thread() + + self._av_loop.stop_thread() + self._main_loop.stop_thread() + + if is_app_closing: + threads.stop_file_transfer_thread() + + # ----------------------------------------------------------------------------------------------------------------- + # Profiles + # ----------------------------------------------------------------------------------------------------------------- + + def _select_profile(self): + self._load_login_screen_translations() + ls = LoginScreen() + profiles = ProfileManager.find_profiles() + ls.update_select(profiles) + ls.show() + self._app.exec_() + + return ls.result + + def _load_existing_profile(self, profile_path): + self._profile_manager = ProfileManager(self._toxes, profile_path) + data = self._profile_manager.open_profile() + if self._toxes.is_data_encrypted(data): + data = self._enter_password(data) + self._settings = Settings(self._toxes, profile_path.replace('.tox', '.json')) + self._tox = self._create_tox(data) + + def _create_new_profile(self, profile_name): + result = self._get_create_profile_screen_result() + if result is None: + return False + if result.save_into_default_folder: + profile_path = util.join_path(Settings.get_default_path(), profile_name + '.tox') + else: + profile_path = util.join_path(util.curr_directory(__file__), profile_name + '.tox') + if os.path.isfile(profile_path): + util_ui.message_box(util_ui.tr('Profile with this name already exists'), + util_ui.tr('Error')) + return False + name = profile_name or 'toxygen_user' + self._tox = tox_factory() + self._tox.self_set_name(name if name else 'Toxygen User') + self._tox.self_set_status_message('Toxing on Toxygen') + self._path = profile_path + if result.password: + self._toxes.set_password(result.password) + self._settings = Settings(self._toxes, self._path.replace('.tox', '.json')) + self._profile_manager = ProfileManager(self._toxes, profile_path) + try: + self._save_profile() + except Exception as ex: + print(ex) + util.log('Profile creation exception: ' + str(ex)) + text = util_ui.tr('Profile saving error! Does Toxygen have permission to write to this directory?') + util_ui.message_box(text, util_ui.tr('Error')) + + return False + current_language, supported_languages = self._get_languages() + if current_language in supported_languages: + self._settings['language'] = current_language + self._settings.save() + + return True + + def _get_create_profile_screen_result(self): + cps = CreateProfileScreen() + cps.show() + self._app.exec_() + + return cps.result + + def _save_profile(self, data=None): + data = data or self._tox.get_savedata() + self._profile_manager.save_profile(data) + + # ----------------------------------------------------------------------------------------------------------------- + # Other private methods + # ----------------------------------------------------------------------------------------------------------------- + + def _enter_password(self, data): + """ + Show password screen + """ + p = password_screen.PasswordScreen(self._toxes, data) + p.show() + self._app.lastWindowClosed.connect(self._app.quit) + self._app.exec_() + if p.result is not None: + return p.result + self._force_exit() + + def _reset(self): + """ + Create new tox instance (new network settings) + :return: tox instance + """ + self._contacts_manager.reset_contacts_statuses() + self._stop_threads(False) + data = self._tox.get_savedata() + self._save_profile(data) + self._kill_toxav() + self._kill_tox() + # create new tox instance + self._tox = self._create_tox(data) + self._start_threads(False) + + tox_savers = [self._friend_factory, self._group_factory, self._plugin_loader, self._contacts_manager, + self._contacts_provider, self._messenger, self._file_transfer_handler, self._groups_service, + self._profile] + for tox_saver in tox_savers: + tox_saver.set_tox(self._tox) + + self._calls_manager.set_toxav(self._tox.AV) + self._contacts_manager.update_friends_numbers() + self._contacts_manager.update_groups_lists() + self._contacts_manager.update_groups_numbers() + + self._init_callbacks() + + def _create_dependencies(self): + self._backup_service = BackupService(self._settings, self._profile_manager) + self._smiley_loader = SmileyLoader(self._settings) + self._tox_dns = ToxDns(self._settings) + self._ms = MainWindow(self._settings, self._tray) + db = Database(self._path.replace('.tox', '.db'), self._toxes) + + contact_items_factory = ContactItemsFactory(self._settings, self._ms) + self._friend_factory = FriendFactory(self._profile_manager, self._settings, + self._tox, db, contact_items_factory) + self._group_factory = GroupFactory(self._profile_manager, self._settings, self._tox, db, contact_items_factory) + self._group_peer_factory = GroupPeerFactory(self._tox, self._profile_manager, db, contact_items_factory) + self._contacts_provider = ContactProvider(self._tox, self._friend_factory, self._group_factory, + self._group_peer_factory) + self._profile = Profile(self._profile_manager, self._tox, self._ms, self._contacts_provider, self._reset) + self._init_profile() + self._plugin_loader = PluginLoader(self._settings, self) + history = None + messages_items_factory = MessagesItemsFactory(self._settings, self._plugin_loader, self._smiley_loader, + self._ms, lambda m: history.delete_message(m)) + history = History(self._contacts_provider, db, self._settings, self._ms, messages_items_factory) + self._contacts_manager = ContactsManager(self._tox, self._settings, self._ms, self._profile_manager, + self._contacts_provider, history, self._tox_dns, + messages_items_factory) + history.set_contacts_manager(self._contacts_manager) + self._calls_manager = CallsManager(self._tox.AV, self._settings, self._ms, self._contacts_manager) + self._messenger = Messenger(self._tox, self._plugin_loader, self._ms, self._contacts_manager, + self._contacts_provider, messages_items_factory, self._profile, + self._calls_manager) + file_transfers_message_service = FileTransfersMessagesService(self._contacts_manager, messages_items_factory, + self._profile, self._ms) + self._file_transfer_handler = FileTransfersHandler(self._tox, self._settings, self._contacts_provider, + file_transfers_message_service, self._profile) + messages_items_factory.set_file_transfers_handler(self._file_transfer_handler) + widgets_factory = None + widgets_factory_provider = Provider(lambda: widgets_factory) + self._groups_service = GroupsService(self._tox, self._contacts_manager, self._contacts_provider, self._ms, + widgets_factory_provider) + widgets_factory = WidgetsFactory(self._settings, self._profile, self._profile_manager, self._contacts_manager, + self._file_transfer_handler, self._smiley_loader, self._plugin_loader, + self._toxes, self._version, self._groups_service, history, + self._contacts_provider) + self._tray = tray.init_tray(self._profile, self._settings, self._ms, self._toxes) + self._ms.set_dependencies(widgets_factory, self._tray, self._contacts_manager, self._messenger, self._profile, + self._plugin_loader, self._file_transfer_handler, history, self._calls_manager, + self._groups_service, self._toxes) + + self._tray.show() + self._ms.show() + + self._init_callbacks() + + def _try_to_update(self): + updating = updater.start_update_if_needed(self._version, self._settings) + if updating: + self._save_profile() + self._settings.close() + self._kill_toxav() + self._kill_tox() + return updating + + def _create_tox(self, data): + return tox_factory(data, self._settings) + + def _force_exit(self): + raise SystemExit() + + def _init_callbacks(self): + callbacks.init_callbacks(self._tox, self._profile, self._settings, self._plugin_loader, self._contacts_manager, + self._calls_manager, self._file_transfer_handler, self._ms, self._tray, + self._messenger, self._groups_service, self._contacts_provider) + + def _init_profile(self): + if not self._profile.has_avatar(): + self._profile.reset_avatar(self._settings['identicons']) + + def _kill_toxav(self): + self._calls_manager.set_toxav(None) + self._tox.AV.kill() + + def _kill_tox(self): + self._tox.kill() diff --git a/toxygen/smileys/__init__.py b/toxygen/smileys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/smileys/smileys.py b/toxygen/smileys/smileys.py new file mode 100644 index 0000000..0391856 --- /dev/null +++ b/toxygen/smileys/smileys.py @@ -0,0 +1,74 @@ +from utils import util +import json +import os +from collections import OrderedDict +from PyQt5 import QtCore + + +class SmileyLoader: + """ + 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 = util.join_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 = util.join_path(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.join_path(util.get_smileys_directory(), self._curr_pack) if self._curr_pack is not None else None + + @staticmethod + def get_packs_list(): + d = util.get_smileys_directory() + 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] = ''.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) diff --git a/toxygen/stickers/__init__.py b/toxygen/stickers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/toxygen/stickers/stickers.py b/toxygen/stickers/stickers.py new file mode 100644 index 0000000..14142c7 --- /dev/null +++ b/toxygen/stickers/stickers.py @@ -0,0 +1,18 @@ +import os +import utils.util as util + + +def load_stickers(): + """ + :return list of stickers + """ + result = [] + d = util.get_stickers_directory() + keys = [x[1] for x in os.walk(d)][0] + for key in keys: + path = util.join_path(d, key) + files = filter(lambda f: f.endswith('.png'), os.listdir(path)) + files = map(lambda f: util.join_path(path, f), files) + result.extend(files) + + return result