From 65a7f2e7d8576ebae7329dc8bab8d2157eea2a71 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 5 Feb 2020 02:50:29 +0300 Subject: [PATCH] Added dictionary management box. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 2 + .../boxes/dictionaries_manager.cpp | 433 ++++++++++++++++++ .../SourceFiles/boxes/dictionaries_manager.h | 36 ++ Telegram/SourceFiles/settings/settings.style | 4 + 5 files changed, 477 insertions(+) create mode 100644 Telegram/SourceFiles/boxes/dictionaries_manager.cpp create mode 100644 Telegram/SourceFiles/boxes/dictionaries_manager.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 5ed03a6523..6877ea1cb7 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -189,6 +189,8 @@ PRIVATE boxes/connection_box.h boxes/create_poll_box.cpp boxes/create_poll_box.h + boxes/dictionaries_manager.cpp + boxes/dictionaries_manager.h boxes/download_path_box.cpp boxes/download_path_box.h boxes/edit_caption_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b15250cf77..ab50a4318c 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -422,6 +422,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_spellchecker" = "Spell checker"; "lng_settings_system_spellchecker" = "Use system spell checker"; +"lng_settings_manage_dictionaries" = "Manage dictionaries"; +"lng_settings_manage_enabled_dictionary" = "Dictionary is enabled"; "lng_backgrounds_header" = "Choose your new chat background"; "lng_theme_sure_keep" = "Keep this theme?"; diff --git a/Telegram/SourceFiles/boxes/dictionaries_manager.cpp b/Telegram/SourceFiles/boxes/dictionaries_manager.cpp new file mode 100644 index 0000000000..8b6d062f94 --- /dev/null +++ b/Telegram/SourceFiles/boxes/dictionaries_manager.cpp @@ -0,0 +1,433 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/dictionaries_manager.h" + +#ifndef TDESKTOP_DISABLE_SPELLCHECK + +#include "mtproto/dedicated_file_loader.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/effects/animations.h" +#include "ui/effects/radial_animation.h" +#include "lang/lang_keys.h" +#include "base/zlib_help.h" +#include "layout.h" +#include "core/application.h" +#include "main/main_account.h" +#include "main/main_session.h" +#include "mainwidget.h" +#include "app.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" + +#include "chat_helpers/spellchecker_common.h" + +#include + +namespace Ui { +namespace { + +using Dictionaries = std::vector; + +struct Available { + int size = 0; + + inline bool operator<(const Available &other) const { + return size < other.size; + } + inline bool operator==(const Available &other) const { + return size == other.size; + } +}; +struct Ready { + inline bool operator<(const Ready &other) const { + return false; + } + inline bool operator==(const Ready &other) const { + return true; + } +}; +struct Active { + inline bool operator<(const Active &other) const { + return false; + } + inline bool operator==(const Active &other) const { + return true; + } +}; +using Loading = MTP::DedicatedLoader::Progress; +struct Failed { + inline bool operator<(const Failed &other) const { + return false; + } + inline bool operator==(const Failed &other) const { + return true; + } +}; +using SetState = base::variant< + Available, + Ready, + Active, + Loading, + Failed>; + +class Loader : public QObject { +public: + Loader(QObject *parent, int id); + + int id() const; + + rpl::producer state() const; + void destroy(); + +private: + void setImplementation(std::unique_ptr loader); + void unpack(const QString &path); + void finalize(const QString &path); + void fail(); + + int _id = 0; + int _size = 0; + rpl::variable _state; + + MTP::WeakInstance _mtproto; + std::unique_ptr _implementation; + +}; + +class Inner : public Ui::RpWidget { +public: + Inner(QWidget *parent, Dictionaries enabledDictionaries); + + Dictionaries enabledRows() const; + +private: + void setupContent(Dictionaries enabledDictionaries); + + Dictionaries _enabledRows; + +}; + +base::unique_qptr GlobalLoader; +rpl::event_stream GlobalLoaderValues; + +QLocale LocaleFromLangId(int langId) { + if (langId > 1000) { + const auto l = langId / 1000; + const auto lang = static_cast(l); + const auto country = static_cast(langId - l * 1000); + return QLocale(lang, country); + } + return QLocale(static_cast(langId)); +} + +void SetGlobalLoader(base::unique_qptr loader) { + GlobalLoader = std::move(loader); + GlobalLoaderValues.fire(GlobalLoader.get()); +} + +int GetDownloadSize(int id) { + const auto sets = Spellchecker::Dictionaries(); + return ranges::find(sets, id, &Spellchecker::Dict::id)->size; +} + +MTP::DedicatedLoader::Location GetDownloadLocation(int id) { + constexpr auto kUsername = "tdhbcfiles"; + const auto sets = Spellchecker::Dictionaries(); + const auto i = ranges::find(sets, id, &Spellchecker::Dict::id); + return MTP::DedicatedLoader::Location{ kUsername, i->postId }; +} + +SetState ComputeState(int id) { + // if (id == CurrentSetId()) { + // return Active(); + if (Spellchecker::DictionaryExists(id)) { + return Ready(); + } + return Available{ GetDownloadSize(id) }; +} + +QString StateDescription(const SetState &state) { + return state.match([](const Available &data) { + return tr::lng_emoji_set_download(tr::now, lt_size, formatSizeText(data.size)); + }, [](const Ready &data) -> QString { + return tr::lng_emoji_set_ready(tr::now); + }, [](const Active &data) -> QString { + return tr::lng_settings_manage_enabled_dictionary(tr::now); + // return tr::lng_emoji_set_active(tr::now); + }, [](const Loading &data) { + const auto percent = (data.size > 0) + ? snap((data.already * 100) / float64(data.size), 0., 100.) + : 0.; + return tr::lng_emoji_set_loading( + tr::now, + lt_percent, + QString::number(int(std::round(percent))) + '%', + lt_progress, + formatDownloadText(data.already, data.size)); + }, [](const Failed &data) { + return tr::lng_attach_failed(tr::now); + }); +} + +Loader::Loader(QObject *parent, int id) +: QObject(parent) +, _id(id) +, _size(GetDownloadSize(_id)) +, _state(Loading{ 0, _size }) +, _mtproto(Core::App().activeAccount().mtp()) { + const auto ready = [=](std::unique_ptr loader) { + if (loader) { + setImplementation(std::move(loader)); + } else { + fail(); + } + }; + const auto location = GetDownloadLocation(id); + const auto folder = Spellchecker::DictPathByLangId(id); + MTP::StartDedicatedLoader(&_mtproto, location, folder, ready); +} + +int Loader::id() const { + return _id; +} + +rpl::producer Loader::state() const { + return _state.value(); +} + +void Loader::setImplementation( + std::unique_ptr loader) { + _implementation = std::move(loader); + auto convert = [](auto value) { + return SetState(value); + }; + _state = _implementation->progress( + ) | rpl::map([](const Loading &state) { + return SetState(state); + }); + _implementation->failed( + ) | rpl::start_with_next([=] { + fail(); + }, _implementation->lifetime()); + + _implementation->ready( + ) | rpl::start_with_next([=](const QString &filepath) { + unpack(filepath); + }, _implementation->lifetime()); + + QDir(Spellchecker::DictPathByLangId(_id)).removeRecursively(); + _implementation->start(); +} + +void Loader::unpack(const QString &path) { + const auto weak = Ui::MakeWeak(this); + crl::async([=] { + if (Spellchecker::UnpackDictionary(path, _id)) { + QFile(path).remove(); + crl::on_main(weak, [=] { + destroy(); + }); + } else { + crl::on_main(weak, [=] { + fail(); + }); + } + }); +} + +void Loader::finalize(const QString &path) { +} + +void Loader::fail() { + _state = Failed(); +} + +void Loader::destroy() { + Expects(GlobalLoader == this); + + SetGlobalLoader(nullptr); +} + +Inner::Inner( + QWidget *parent, + Dictionaries enabledDictionaries) : RpWidget(parent) { + setupContent(std::move(enabledDictionaries)); +} + +Dictionaries Inner::enabledRows() const { + return _enabledRows; +} + +auto AddButtonWithLoader( + not_null content, + const Spellchecker::Dict &set, + bool buttonEnabled) { + const auto id = set.id; + + const auto button = content->add( + object_ptr>( + content, + object_ptr( + content, + rpl::single(set.name), + st::dictionariesSectionButton + ) + ) + )->entity(); + + const auto buttonState = button->lifetime() + .make_state>(); + + const auto label = Ui::CreateChild( + button, + buttonState->value() | rpl::map(StateDescription), + st::settingsUpdateState); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + + rpl::combine( + button->widthValue(), + label->widthValue() + ) | rpl::start_with_next([=] { + label->moveToLeft( + st::settingsUpdateStatePosition.x(), + st::settingsUpdateStatePosition.y()); + }, label->lifetime()); + + buttonState->value( + ) | rpl::start_with_next([=](const SetState &state) { + const auto isToggledSet = state.is(); + const auto toggled = isToggledSet ? 1. : 0.; + const auto over = !button->isDisabled() + && (button->isDown() || button->isOver()); + + if (toggled == 0. && !over) { + label->setTextColorOverride(std::nullopt); + } else { + label->setTextColorOverride(anim::color( + over ? st::contactsStatusFgOver : st::contactsStatusFg, + st::contactsStatusFgOnline, + toggled)); + } + }, label->lifetime()); + + button->toggleOn( + rpl::single( + buttonEnabled + ) | rpl::then( + buttonState->value( + ) | rpl::filter([](const SetState &state) { + return state.is(); + }) | rpl::map([](const SetState &state) { + return false; + }) + ) + ); + + *buttonState = GlobalLoaderValues.events_starting_with( + GlobalLoader.get() + ) | rpl::map([=](Loader *loader) { + return (loader && loader->id() == id) + ? loader->state() + : rpl::single( + buttonEnabled + ) | rpl::then( + button->toggledValue() + ) | rpl::map([=](auto enabled) { + const auto &state = buttonState->current(); + if (enabled && state.is()) { + return SetState(Active()); + } + if (!enabled && state.is()) { + return SetState(Ready()); + } + return ComputeState(id); + }); + }) | rpl::flatten_latest( + ) | rpl::filter([=](const SetState &state) { + return !buttonState->current().is() || !state.is(); + }); + + button->toggledValue( + ) | rpl::start_with_next([=](bool toggled) { + const auto &state = buttonState->current(); + if (toggled && (state.is() || state.is())) { + SetGlobalLoader(base::make_unique_q(App::main(), id)); + } else if (!toggled && state.is()) { + if (GlobalLoader && GlobalLoader->id() == id) { + GlobalLoader->destroy(); + } + } + }, button->lifetime()); + + return button; +} + +void Inner::setupContent(Dictionaries enabledDictionaries) { + const auto content = Ui::CreateChild(this); + + const auto sets = Spellchecker::Dictionaries(); + for (const auto &set : sets) { + const auto row = AddButtonWithLoader( + content, + set, + ranges::contains(enabledDictionaries, set.id)); + row->toggledValue( + ) | rpl::start_with_next([=](auto enabled) { + if (enabled && Spellchecker::DictionaryExists(set.id)) { + _enabledRows.push_back(set.id); + } else { + auto &rows = _enabledRows; + rows.erase(ranges::remove(rows, set.id), end(rows)); + } + }, row->lifetime()); + } + + content->resizeToWidth(st::boxWidth); + Ui::ResizeFitChild(this, content); +} + +} // namespace + +ManageDictionariesBox::ManageDictionariesBox( + QWidget*, + not_null session) +: _session(session) { +} + +void ManageDictionariesBox::prepare() { + const auto inner = setInnerWidget(object_ptr( + this, + _session->settings().dictionariesEnabled())); + + setTitle(tr::lng_settings_manage_dictionaries()); + + addButton(tr::lng_settings_save(), [=] { + _session->settings().setDictionariesEnabled(inner->enabledRows()); + _session->saveSettingsDelayed(); + closeBox(); + }); + addButton(tr::lng_close(), [=] { closeBox(); }); + + setDimensionsToContent(st::boxWidth, inner); + + inner->heightValue( + ) | rpl::start_with_next([=](int height) { + using std::min; + setDimensions(st::boxWidth, min(height, st::boxMaxListHeight)); + }, inner->lifetime()); +} + +} // namespace Ui + +#endif // !TDESKTOP_DISABLE_SPELLCHECK diff --git a/Telegram/SourceFiles/boxes/dictionaries_manager.h b/Telegram/SourceFiles/boxes/dictionaries_manager.h new file mode 100644 index 0000000000..f819e140dd --- /dev/null +++ b/Telegram/SourceFiles/boxes/dictionaries_manager.h @@ -0,0 +1,36 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#ifndef TDESKTOP_DISABLE_SPELLCHECK + +#include "boxes/abstract_box.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { + +class ManageDictionariesBox : public Ui::BoxContent { +public: + ManageDictionariesBox( + QWidget*, + not_null session); + +protected: + void prepare() override; + +private: + const not_null _session; + +}; + +} // namespace Ui + +#endif // !TDESKTOP_DISABLE_SPELLCHECK diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 9d1040125b..0500f28088 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -215,3 +215,7 @@ settingsForwardPrivacyTooltipPadding: margins(8px, 6px, 8px, 6px); settingsAccentColorSize: 24px; settingsAccentColorSkip: 4px; settingsAccentColorLine: 3px; + +dictionariesSectionButton: SettingsButton(settingsUpdateToggle) { + font: font(14px semibold); +}