init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
567
Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp
Normal file
567
Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp
Normal file
@@ -0,0 +1,567 @@
|
||||
/*
|
||||
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 u":/gui/emoji/set%1_preview.webp"_q.arg(i);
|
||||
}
|
||||
|
||||
const auto kSets = {
|
||||
Set{ { 0, 0, 0, "Mac" }, PreviewPath(0) },
|
||||
Set{ { 1, 2774, 8'455'034, "Android" }, PreviewPath(1) },
|
||||
Set{ { 2, 2775, 5'713'503, "Twemoji" }, PreviewPath(2) },
|
||||
Set{ { 3, 2776, 7'347'332, "JoyPixels" }, PreviewPath(3) },
|
||||
};
|
||||
|
||||
using Loading = MTP::DedicatedLoader::Progress;
|
||||
using SetState = BlobState;
|
||||
|
||||
class Loader final : public BlobLoader {
|
||||
public:
|
||||
Loader(
|
||||
not_null<Main::Session*> 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<Main::Session*> session);
|
||||
|
||||
private:
|
||||
void setupContent();
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
|
||||
};
|
||||
|
||||
class Row : public Ui::RippleButton {
|
||||
public:
|
||||
Row(QWidget *widget, not_null<Main::Session*> 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<Main::Session*> _session;
|
||||
int _id = 0;
|
||||
bool _switching = false;
|
||||
rpl::variable<SetState> _state;
|
||||
Ui::FlatLabel *_status = nullptr;
|
||||
std::array<QPixmap, 4> _preview;
|
||||
Ui::Animations::Simple _toggled;
|
||||
Ui::Animations::Simple _active;
|
||||
std::unique_ptr<Ui::RadialAnimation> _loading;
|
||||
|
||||
};
|
||||
|
||||
base::unique_qptr<Loader> GlobalLoader;
|
||||
rpl::event_stream<Loader*> GlobalLoaderValues;
|
||||
|
||||
void SetGlobalLoader(base::unique_qptr<Loader> 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<const Loading*> 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<Main::Session*> 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 = base::make_weak(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<Main::Session*> session)
|
||||
: RpWidget(parent)
|
||||
, _session(session) {
|
||||
setupContent();
|
||||
}
|
||||
|
||||
void Inner::setupContent() {
|
||||
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
|
||||
|
||||
for (const auto &set : kSets) {
|
||||
content->add(object_ptr<Row>(content, _session, set));
|
||||
}
|
||||
|
||||
content->resizeToWidth(st::boxWidth);
|
||||
Ui::ResizeFitChild(this, content);
|
||||
}
|
||||
|
||||
Row::Row(QWidget *widget, not_null<Main::Session*> 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, arc::kFullLength };
|
||||
const auto isToggledSet = v::is<Active>(_state.current());
|
||||
const auto isActiveSet = isToggledSet || v::is<Loading>(_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 < arc::kFullLength) {
|
||||
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<Active>(_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<Failed>(_state.current())
|
||||
|| !v::is<Available>(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<Ready>(state)
|
||||
|| v::is<Available>(state));
|
||||
}) | rpl::on_next([=] {
|
||||
if (v::is<Available>(_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<Ready>(state) || v::is<Available>(state);
|
||||
}) | rpl::on_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<Ui::FlatLabel>(
|
||||
this,
|
||||
set.name,
|
||||
st::localStorageRowTitle);
|
||||
name->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
_status = Ui::CreateChild<Ui::FlatLabel>(
|
||||
this,
|
||||
_state.value() | rpl::map(StateDescription),
|
||||
st::localStorageRowSize);
|
||||
_status->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_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 * style::DevicePixelRatio();
|
||||
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(style::DevicePixelRatio());
|
||||
}
|
||||
}
|
||||
|
||||
void Row::updateLoadingToFinished() {
|
||||
_loading->update(
|
||||
v::is<Failed>(_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<Loading>(&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::on_next([=](const SetState &state) {
|
||||
update();
|
||||
}, lifetime());
|
||||
|
||||
_state.value(
|
||||
) | rpl::map(
|
||||
_1 == SetState{ Active() }
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::on_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<Loading>(state) || v::is<Active>(state);
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::on_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<Loading>(&state);
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::on_next([=](const Loading *loading) {
|
||||
if (loading && !_loading) {
|
||||
_loading = std::make_unique<Ui::RadialAnimation>(
|
||||
[=](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<Main::Session*> session)
|
||||
: _session(session) {
|
||||
}
|
||||
|
||||
void ManageSetsBox::prepare() {
|
||||
const auto inner = setInnerWidget(object_ptr<Inner>(this, _session));
|
||||
|
||||
setTitle(tr::lng_emoji_manage_sets());
|
||||
|
||||
addButton(tr::lng_close(), [=] { closeBox(); });
|
||||
|
||||
setDimensionsToContent(st::boxWidth, inner);
|
||||
}
|
||||
|
||||
void LoadAndSwitchTo(not_null<Main::Session*> session, int id) {
|
||||
if (!ranges::contains(kSets, id, &Set::id)) {
|
||||
ClearNeedSwitchToId();
|
||||
return;
|
||||
}
|
||||
SetGlobalLoader(base::make_unique_q<Loader>(
|
||||
session,
|
||||
id,
|
||||
GetDownloadLocation(id),
|
||||
internal::SetDataPath(id),
|
||||
GetDownloadSize(id)));
|
||||
}
|
||||
|
||||
} // namespace Emoji
|
||||
} // namespace Ui
|
||||
Reference in New Issue
Block a user