/* 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 "chat_helpers/emoji_sets_manager.h" #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/effects/animations.h" #include "ui/effects/radial_animation.h" #include "ui/emoji_config.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "core/application.h" #include "lang/lang_keys.h" #include "main/main_account.h" #include "storage/storage_cloud_blob.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" namespace Ui { namespace Emoji { namespace { using namespace Storage::CloudBlob; struct Set : public Blob { QString previewPath; }; inline auto PreviewPath(int i) { return qsl(":/gui/emoji/set%1_preview.webp").arg(i); } const auto kSets = { Set{ { 0, 0, 0, "Mac" }, PreviewPath(0) }, Set{ { 1, 1392, 8'184'590, "Android" }, PreviewPath(1) }, Set{ { 2, 1393, 5'413'219, "Twemoji" }, PreviewPath(2) }, Set{ { 3, 1394, 6'967'218, "JoyPixels" }, PreviewPath(3) }, }; using Loading = MTP::DedicatedLoader::Progress; using SetState = BlobState; class Loader final : public BlobLoader { public: Loader( not_null session, int id, MTP::DedicatedLoader::Location location, const QString &folder, int size); void destroy() override; void unpack(const QString &path) override; private: void fail() override; }; class Inner : public Ui::RpWidget { public: Inner(QWidget *parent, not_null session); private: void setupContent(); const not_null _session; }; class Row : public Ui::RippleButton { public: Row(QWidget *widget, not_null session, const Set &set); protected: void paintEvent(QPaintEvent *e) override; void onStateChanged(State was, StateChangeSource source) override; private: [[nodiscard]] bool showOver() const; [[nodiscard]] bool showOver(State state) const; void updateStatusColorOverride(); void setupContent(const Set &set); void setupLabels(const Set &set); void setupPreview(const Set &set); void setupAnimation(); void paintPreview(QPainter &p) const; void paintRadio(QPainter &p); void setupHandler(); void load(); void radialAnimationCallback(crl::time now); void updateLoadingToFinished(); const not_null _session; int _id = 0; bool _switching = false; rpl::variable _state; Ui::FlatLabel *_status = nullptr; std::array _preview; Ui::Animations::Simple _toggled; Ui::Animations::Simple _active; std::unique_ptr _loading; }; base::unique_qptr GlobalLoader; rpl::event_stream GlobalLoaderValues; void SetGlobalLoader(base::unique_qptr loader) { GlobalLoader = std::move(loader); GlobalLoaderValues.fire(GlobalLoader.get()); } int64 GetDownloadSize(int id) { return ranges::find(kSets, id, &Set::id)->size; } [[nodiscard]] float64 CountProgress(not_null loading) { return (loading->size > 0) ? (loading->already / float64(loading->size)) : 0.; } MTP::DedicatedLoader::Location GetDownloadLocation(int id) { const auto username = kCloudLocationUsername.utf16(); const auto i = ranges::find(kSets, id, &Set::id); return MTP::DedicatedLoader::Location{ username, i->postId }; } SetState ComputeState(int id) { if (id == CurrentSetId()) { return Active(); } else if (SetIsReady(id)) { return Ready(); } return Available{ GetDownloadSize(id) }; } QString StateDescription(const SetState &state) { return StateDescription( state, tr::lng_emoji_set_active); } bool GoodSetPartName(const QString &name) { return (name == u"config.json"_q) || (name.startsWith(u"emoji_"_q) && name.endsWith(u".webp"_q)); } bool UnpackSet(const QString &path, const QString &folder) { return UnpackBlob(path, folder, GoodSetPartName); } Loader::Loader( not_null session, int id, MTP::DedicatedLoader::Location location, const QString &folder, int size) : BlobLoader(nullptr, session, id, location, folder, size) { } void Loader::unpack(const QString &path) { const auto folder = internal::SetDataPath(id()); const auto weak = Ui::MakeWeak(this); crl::async([=] { if (UnpackSet(path, folder)) { QFile(path).remove(); SwitchToSet(id(), crl::guard(weak, [=](bool success) { if (success) { destroy(); } else { fail(); } })); } else { crl::on_main(weak, [=] { fail(); }); } }); } void Loader::destroy() { Expects(GlobalLoader == this); SetGlobalLoader(nullptr); } void Loader::fail() { ClearNeedSwitchToId(); BlobLoader::fail(); } Inner::Inner(QWidget *parent, not_null session) : RpWidget(parent) , _session(session) { setupContent(); } void Inner::setupContent() { const auto content = Ui::CreateChild(this); for (const auto &set : kSets) { content->add(object_ptr(content, _session, set)); } content->resizeToWidth(st::boxWidth); Ui::ResizeFitChild(this, content); } Row::Row(QWidget *widget, not_null session, const Set &set) : RippleButton(widget, st::defaultRippleAnimation) , _session(session) , _id(set.id) , _state(Available{ set.size }) { setupContent(set); setupHandler(); } void Row::paintEvent(QPaintEvent *e) { auto p = QPainter(this); const auto over = showOver(); const auto bg = over ? st::windowBgOver : st::windowBg; p.fillRect(rect(), bg); paintRipple(p, 0, 0); paintPreview(p); paintRadio(p); } void Row::paintPreview(QPainter &p) const { const auto x = st::manageEmojiPreviewPadding.left(); const auto y = st::manageEmojiPreviewPadding.top(); const auto width = st::manageEmojiPreviewWidth; const auto height = st::manageEmojiPreviewWidth; auto &&preview = ranges::views::zip(_preview, ranges::views::ints(0, int(_preview.size()))); for (const auto [pixmap, index] : preview) { const auto row = (index / 2); const auto column = (index % 2); const auto left = x + (column ? width - st::manageEmojiPreview : 0); const auto top = y + (row ? height - st::manageEmojiPreview : 0); p.drawPixmap(left, top, pixmap); } } void Row::paintRadio(QPainter &p) { if (_loading && !_loading->animating()) { _loading = nullptr; } const auto loading = _loading ? _loading->computeState() : Ui::RadialState{ 0., 0, FullArcLength }; const auto isToggledSet = v::is(_state.current()); const auto isActiveSet = isToggledSet || v::is(_state.current()); const auto toggled = _toggled.value(isToggledSet ? 1. : 0.); const auto active = _active.value(isActiveSet ? 1. : 0.); const auto _st = &st::defaultRadio; PainterHighQualityEnabler hq(p); const auto left = width() - st::manageEmojiMarginRight - _st->diameter - _st->thickness; const auto top = (height() - _st->diameter - _st->thickness) / 2; const auto outerWidth = width(); auto pen = anim::pen(_st->untoggledFg, _st->toggledFg, active); pen.setWidth(_st->thickness); pen.setCapStyle(Qt::RoundCap); p.setPen(pen); p.setBrush(_st->bg); const auto rect = style::rtlrect(QRectF( left, top, _st->diameter, _st->diameter ).marginsRemoved(QMarginsF( _st->thickness / 2., _st->thickness / 2., _st->thickness / 2., _st->thickness / 2. )), outerWidth); if (loading.shown > 0 && anim::Disabled()) { anim::DrawStaticLoading( p, rect, _st->thickness, pen.color(), _st->bg); } else if (loading.arcLength < FullArcLength) { p.drawArc(rect, loading.arcFrom, loading.arcLength); } else { p.drawEllipse(rect); } if (toggled > 0 && (!_loading || !anim::Disabled())) { p.setPen(Qt::NoPen); p.setBrush(anim::brush(_st->untoggledFg, _st->toggledFg, toggled)); const auto skip0 = _st->diameter / 2.; const auto skip1 = _st->skip / 10.; const auto checkSkip = skip0 * (1. - toggled) + skip1 * toggled; p.drawEllipse(style::rtlrect(QRectF( left, top, _st->diameter, _st->diameter ).marginsRemoved(QMarginsF( checkSkip, checkSkip, checkSkip, checkSkip )), outerWidth)); } } bool Row::showOver(State state) const { return (!(state & StateFlag::Disabled)) && (state & (StateFlag::Over | StateFlag::Down)); } bool Row::showOver() const { return showOver(state()); } void Row::onStateChanged(State was, StateChangeSource source) { RippleButton::onStateChanged(was, source); if (showOver() != showOver(was)) { updateStatusColorOverride(); } } void Row::updateStatusColorOverride() { const auto isToggledSet = v::is(_state.current()); const auto toggled = _toggled.value(isToggledSet ? 1. : 0.); const auto over = showOver(); if (toggled == 0. && !over) { _status->setTextColorOverride(std::nullopt); } else { _status->setTextColorOverride(anim::color( over ? st::contactsStatusFgOver : st::contactsStatusFg, st::contactsStatusFgOnline, toggled)); } } void Row::setupContent(const Set &set) { _state = GlobalLoaderValues.events_starting_with( GlobalLoader.get() ) | rpl::map([=](Loader *loader) { return (loader && loader->id() == _id) ? loader->state() : rpl::single(rpl::empty) | rpl::then( Updated() ) | rpl::map([=] { return ComputeState(_id); }); }) | rpl::flatten_latest( ) | rpl::filter([=](const SetState &state) { return !v::is(_state.current()) || !v::is(state); }); setupLabels(set); setupPreview(set); setupAnimation(); const auto height = st::manageEmojiPreviewPadding.top() + st::manageEmojiPreviewHeight + st::manageEmojiPreviewPadding.bottom(); resize(width(), height); } void Row::setupHandler() { clicks( ) | rpl::filter([=] { const auto &state = _state.current(); return !_switching && (v::is(state) || v::is(state)); }) | rpl::start_with_next([=] { if (v::is(_state.current())) { load(); return; } _switching = true; SwitchToSet(_id, crl::guard(this, [=](bool success) { _switching = false; if (!success) { load(); } else if (GlobalLoader && GlobalLoader->id() == _id) { GlobalLoader->destroy(); } })); }, lifetime()); _state.value( ) | rpl::map([=](const SetState &state) { return v::is(state) || v::is(state); }) | rpl::start_with_next([=](bool active) { setDisabled(!active); setPointerCursor(active); }, lifetime()); } void Row::load() { LoadAndSwitchTo(_session, _id); } void Row::setupLabels(const Set &set) { using namespace rpl::mappers; const auto name = Ui::CreateChild( this, set.name, st::localStorageRowTitle); name->setAttribute(Qt::WA_TransparentForMouseEvents); _status = Ui::CreateChild( this, _state.value() | rpl::map(StateDescription), st::localStorageRowSize); _status->setAttribute(Qt::WA_TransparentForMouseEvents); sizeValue( ) | rpl::start_with_next([=](QSize size) { const auto left = st::manageEmojiPreviewPadding.left() + st::manageEmojiPreviewWidth + st::manageEmojiPreviewPadding.right(); const auto namey = st::manageEmojiPreviewPadding.top() + st::manageEmojiNameTop; const auto statusy = st::manageEmojiPreviewPadding.top() + st::manageEmojiStatusTop; name->moveToLeft(left, namey); _status->moveToLeft(left, statusy); }, name->lifetime()); } void Row::setupPreview(const Set &set) { const auto size = st::manageEmojiPreview * cIntRetinaFactor(); const auto original = QImage(set.previewPath); const auto full = original.height(); auto &&preview = ranges::views::zip(_preview, ranges::views::ints(0, int(_preview.size()))); for (auto &&[pixmap, index] : preview) { pixmap = Ui::PixmapFromImage(original.copy( { full * index, 0, full, full } ).scaledToWidth(size, Qt::SmoothTransformation)); pixmap.setDevicePixelRatio(cRetinaFactor()); } } void Row::updateLoadingToFinished() { _loading->update( v::is(_state.current()) ? 0. : 1., true, crl::now()); } void Row::radialAnimationCallback(crl::time now) { const auto updated = [&] { const auto state = _state.current(); if (const auto loading = std::get_if(&state)) { return _loading->update(CountProgress(loading), false, now); } else { updateLoadingToFinished(); } return false; }(); if (!anim::Disabled() || updated) { update(); } } void Row::setupAnimation() { using namespace rpl::mappers; _state.value( ) | rpl::start_with_next([=](const SetState &state) { update(); }, lifetime()); _state.value( ) | rpl::map( _1 == SetState{ Active() } ) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](bool toggled) { _toggled.start( [=] { updateStatusColorOverride(); update(); }, toggled ? 0. : 1., toggled ? 1. : 0., st::defaultRadio.duration); }, lifetime()); _state.value( ) | rpl::map([](const SetState &state) { return v::is(state) || v::is(state); }) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](bool active) { _active.start( [=] { update(); }, active ? 0. : 1., active ? 1. : 0., st::defaultRadio.duration); }, lifetime()); _state.value( ) | rpl::map([](const SetState &state) { return std::get_if(&state); }) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](const Loading *loading) { if (loading && !_loading) { _loading = std::make_unique( [=](crl::time now) { radialAnimationCallback(now); }); _loading->start(CountProgress(loading)); } else if (!loading && _loading) { updateLoadingToFinished(); } }, lifetime()); _toggled.stop(); _active.stop(); updateStatusColorOverride(); } } // namespace ManageSetsBox::ManageSetsBox(QWidget*, not_null session) : _session(session) { } void ManageSetsBox::prepare() { const auto inner = setInnerWidget(object_ptr(this, _session)); setTitle(tr::lng_emoji_manage_sets()); addButton(tr::lng_close(), [=] { closeBox(); }); setDimensionsToContent(st::boxWidth, inner); } void LoadAndSwitchTo(not_null session, int id) { if (!ranges::contains(kSets, id, &Set::id)) { ClearNeedSwitchToId(); return; } SetGlobalLoader(base::make_unique_q( session, id, GetDownloadLocation(id), internal::SetDataPath(id), GetDownloadSize(id))); } } // namespace Emoji } // namespace Ui