671 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			671 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
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 "lang/lang_keys.h"
 | 
						|
#include "base/zlib_help.h"
 | 
						|
#include "layout.h"
 | 
						|
#include "core/application.h"
 | 
						|
#include "main/main_account.h"
 | 
						|
#include "mainwidget.h"
 | 
						|
#include "app.h"
 | 
						|
#include "styles/style_boxes.h"
 | 
						|
#include "styles/style_chat_helpers.h"
 | 
						|
 | 
						|
namespace Ui {
 | 
						|
namespace Emoji {
 | 
						|
namespace {
 | 
						|
 | 
						|
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<SetState> state() const;
 | 
						|
	void destroy();
 | 
						|
 | 
						|
private:
 | 
						|
	void setImplementation(std::unique_ptr<MTP::DedicatedLoader> loader);
 | 
						|
	void unpack(const QString &path);
 | 
						|
	void finalize(const QString &path);
 | 
						|
	void fail();
 | 
						|
 | 
						|
	int _id = 0;
 | 
						|
	int _size = 0;
 | 
						|
	rpl::variable<SetState> _state;
 | 
						|
 | 
						|
	MTP::WeakInstance _mtproto;
 | 
						|
	std::unique_ptr<MTP::DedicatedLoader> _implementation;
 | 
						|
 | 
						|
};
 | 
						|
 | 
						|
class Inner : public Ui::RpWidget {
 | 
						|
public:
 | 
						|
	Inner(QWidget *parent);
 | 
						|
 | 
						|
private:
 | 
						|
	void setupContent();
 | 
						|
 | 
						|
};
 | 
						|
 | 
						|
class Row : public Ui::RippleButton {
 | 
						|
public:
 | 
						|
	Row(QWidget *widget, 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(Painter &p) const;
 | 
						|
	void paintRadio(Painter &p);
 | 
						|
	void setupHandler();
 | 
						|
	void load();
 | 
						|
	void radialAnimationCallback(crl::time now);
 | 
						|
 | 
						|
	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());
 | 
						|
}
 | 
						|
 | 
						|
int GetDownloadSize(int id) {
 | 
						|
	const auto sets = Sets();
 | 
						|
	return ranges::find(sets, id, &Set::id)->size;
 | 
						|
}
 | 
						|
 | 
						|
MTP::DedicatedLoader::Location GetDownloadLocation(int id) {
 | 
						|
	constexpr auto kUsername = "tdhbcfiles";
 | 
						|
	const auto sets = Sets();
 | 
						|
	const auto i = ranges::find(sets, id, &Set::id);
 | 
						|
	return MTP::DedicatedLoader::Location{ kUsername, 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 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_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);
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
QByteArray ReadFinalFile(const QString &path) {
 | 
						|
	constexpr auto kMaxZipSize = 10 * 1024 * 1024;
 | 
						|
	auto file = QFile(path);
 | 
						|
	if (file.size() > kMaxZipSize || !file.open(QIODevice::ReadOnly)) {
 | 
						|
		return QByteArray();
 | 
						|
	}
 | 
						|
	return file.readAll();
 | 
						|
}
 | 
						|
 | 
						|
bool ExtractZipFile(zlib::FileToRead &zip, const QString path) {
 | 
						|
	constexpr auto kMaxSize = 10 * 1024 * 1024;
 | 
						|
	const auto content = zip.readCurrentFileContent(kMaxSize);
 | 
						|
	if (content.isEmpty() || zip.error() != UNZ_OK) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
	auto file = QFile(path);
 | 
						|
	return file.open(QIODevice::WriteOnly)
 | 
						|
		&& (file.write(content) == content.size());
 | 
						|
}
 | 
						|
 | 
						|
bool GoodSetPartName(const QString &name) {
 | 
						|
	return (name == qstr("config.json"))
 | 
						|
		|| (name.startsWith(qstr("emoji_"))
 | 
						|
			&& name.endsWith(qstr(".webp")));
 | 
						|
}
 | 
						|
 | 
						|
bool UnpackSet(const QString &path, const QString &folder) {
 | 
						|
	const auto bytes = ReadFinalFile(path);
 | 
						|
	if (bytes.isEmpty()) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	auto zip = zlib::FileToRead(bytes);
 | 
						|
	if (zip.goToFirstFile() != UNZ_OK) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
	do {
 | 
						|
		const auto name = zip.getCurrentFileName();
 | 
						|
		const auto path = folder + '/' + name;
 | 
						|
		if (GoodSetPartName(name) && !ExtractZipFile(zip, path)) {
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
 | 
						|
		const auto jump = zip.goToNextFile();
 | 
						|
		if (jump == UNZ_END_OF_LIST_OF_FILE) {
 | 
						|
			break;
 | 
						|
		} else if (jump != UNZ_OK) {
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
	} while (true);
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
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<MTP::DedicatedLoader> loader) {
 | 
						|
		if (loader) {
 | 
						|
			setImplementation(std::move(loader));
 | 
						|
		} else {
 | 
						|
			fail();
 | 
						|
		}
 | 
						|
	};
 | 
						|
	const auto location = GetDownloadLocation(id);
 | 
						|
	const auto folder = internal::SetDataPath(id);
 | 
						|
	MTP::StartDedicatedLoader(&_mtproto, location, folder, ready);
 | 
						|
}
 | 
						|
 | 
						|
int Loader::id() const {
 | 
						|
	return _id;
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<SetState> Loader::state() const {
 | 
						|
	return _state.value();
 | 
						|
}
 | 
						|
 | 
						|
void Loader::setImplementation(
 | 
						|
		std::unique_ptr<MTP::DedicatedLoader> 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(internal::SetDataPath(_id)).removeRecursively();
 | 
						|
	_implementation->start();
 | 
						|
}
 | 
						|
 | 
						|
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::finalize(const QString &path) {
 | 
						|
}
 | 
						|
 | 
						|
void Loader::fail() {
 | 
						|
	_state = Failed();
 | 
						|
}
 | 
						|
 | 
						|
void Loader::destroy() {
 | 
						|
	Expects(GlobalLoader == this);
 | 
						|
 | 
						|
	SetGlobalLoader(nullptr);
 | 
						|
}
 | 
						|
 | 
						|
Inner::Inner(QWidget *parent) : RpWidget(parent) {
 | 
						|
	setupContent();
 | 
						|
}
 | 
						|
 | 
						|
void Inner::setupContent() {
 | 
						|
	const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
 | 
						|
 | 
						|
	const auto sets = Sets();
 | 
						|
	for (const auto &set : sets) {
 | 
						|
		content->add(object_ptr<Row>(content, set));
 | 
						|
	}
 | 
						|
 | 
						|
	content->resizeToWidth(st::boxWidth);
 | 
						|
	Ui::ResizeFitChild(this, content);
 | 
						|
}
 | 
						|
 | 
						|
Row::Row(QWidget *widget, const Set &set)
 | 
						|
: RippleButton(widget, st::contactsRipple)
 | 
						|
, _id(set.id)
 | 
						|
, _state(Available{ set.size }) {
 | 
						|
	setupContent(set);
 | 
						|
	setupHandler();
 | 
						|
}
 | 
						|
 | 
						|
void Row::paintEvent(QPaintEvent *e) {
 | 
						|
	Painter p(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(Painter &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::view::zip(_preview, ranges::view::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(Painter &p) {
 | 
						|
	const auto loading = _loading
 | 
						|
		? _loading->computeState()
 | 
						|
		: Ui::RadialState{ 0., 0, FullArcLength };
 | 
						|
	const auto isToggledSet = _state.current().is<Active>();
 | 
						|
	const auto isActiveSet = isToggledSet || _state.current().is<Loading>();
 | 
						|
	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 = _state.current().is<Active>();
 | 
						|
	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_value()
 | 
						|
			) | rpl::then(
 | 
						|
				Updated()
 | 
						|
			) | rpl::map([=] {
 | 
						|
				return ComputeState(_id);
 | 
						|
			});
 | 
						|
	}) | rpl::flatten_latest(
 | 
						|
	) | rpl::filter([=](const SetState &state) {
 | 
						|
		return !_state.current().is<Failed>() || !state.is<Available>();
 | 
						|
	});
 | 
						|
 | 
						|
	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 && (state.is<Ready>() || state.is<Available>());
 | 
						|
	}) | rpl::start_with_next([=] {
 | 
						|
		if (_state.current().is<Available>()) {
 | 
						|
			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 state.is<Ready>() || state.is<Available>();
 | 
						|
	}) | rpl::start_with_next([=](bool active) {
 | 
						|
		setDisabled(!active);
 | 
						|
		setPointerCursor(active);
 | 
						|
	}, lifetime());
 | 
						|
}
 | 
						|
 | 
						|
void Row::load() {
 | 
						|
	SetGlobalLoader(base::make_unique_q<Loader>(App::main(), _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::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::view::zip(_preview, ranges::view::ints(0, int(_preview.size())));
 | 
						|
	for (auto &&[pixmap, index] : preview) {
 | 
						|
		pixmap = App::pixmapFromImageInPlace(original.copy(
 | 
						|
			{ full * index, 0, full, full }
 | 
						|
		).scaledToWidth(size, Qt::SmoothTransformation));
 | 
						|
		pixmap.setDevicePixelRatio(cRetinaFactor());
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void Row::radialAnimationCallback(crl::time now) {
 | 
						|
	const auto updated = [&] {
 | 
						|
		const auto state = _state.current();
 | 
						|
		if (const auto loading = base::get_if<Loading>(&state)) {
 | 
						|
			const auto progress = (loading->size > 0)
 | 
						|
				? (loading->already / float64(loading->size))
 | 
						|
				: 0.;
 | 
						|
			return _loading->update(progress, false, now);
 | 
						|
		}
 | 
						|
		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 == 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 state.is<Loading>() || state.is<Active>();
 | 
						|
	}) | 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 base::get_if<Loading>(&state);
 | 
						|
	}) | rpl::distinct_until_changed(
 | 
						|
	) | rpl::start_with_next([=](const Loading *loading) {
 | 
						|
		if (loading && !_loading) {
 | 
						|
			_loading = std::make_unique<Ui::RadialAnimation>(
 | 
						|
				[=](crl::time now) { radialAnimationCallback(now); });
 | 
						|
			const auto progress = (loading->size > 0)
 | 
						|
				? (loading->already / float64(loading->size))
 | 
						|
				: 0.;
 | 
						|
			_loading->start(progress);
 | 
						|
		} else if (!loading && _loading) {
 | 
						|
			_loading->update(
 | 
						|
				_state.current().is<Failed>() ? 0. : 1.,
 | 
						|
				true,
 | 
						|
				crl::now());
 | 
						|
		}
 | 
						|
	}, lifetime());
 | 
						|
 | 
						|
	_toggled.stop();
 | 
						|
	_active.stop();
 | 
						|
	updateStatusColorOverride();
 | 
						|
}
 | 
						|
 | 
						|
} // namespace
 | 
						|
 | 
						|
ManageSetsBox::ManageSetsBox(QWidget*) {
 | 
						|
}
 | 
						|
 | 
						|
void ManageSetsBox::prepare() {
 | 
						|
	const auto inner = setInnerWidget(object_ptr<Inner>(this));
 | 
						|
 | 
						|
	setTitle(tr::lng_emoji_manage_sets());
 | 
						|
 | 
						|
	addButton(tr::lng_close(), [=] { closeBox(); });
 | 
						|
 | 
						|
	setDimensionsToContent(st::boxWidth, inner);
 | 
						|
}
 | 
						|
 | 
						|
} // namespace Emoji
 | 
						|
} // namespace Ui
 |