emoji_sets_manager.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "chat_helpers/emoji_sets_manager.h"
  8. #include "mtproto/dedicated_file_loader.h"
  9. #include "ui/wrap/vertical_layout.h"
  10. #include "ui/wrap/fade_wrap.h"
  11. #include "ui/widgets/buttons.h"
  12. #include "ui/widgets/labels.h"
  13. #include "ui/effects/animations.h"
  14. #include "ui/effects/radial_animation.h"
  15. #include "ui/emoji_config.h"
  16. #include "ui/painter.h"
  17. #include "ui/ui_utility.h"
  18. #include "core/application.h"
  19. #include "lang/lang_keys.h"
  20. #include "main/main_account.h"
  21. #include "storage/storage_cloud_blob.h"
  22. #include "styles/style_layers.h"
  23. #include "styles/style_boxes.h"
  24. #include "styles/style_chat_helpers.h"
  25. namespace Ui {
  26. namespace Emoji {
  27. namespace {
  28. using namespace Storage::CloudBlob;
  29. struct Set : public Blob {
  30. QString previewPath;
  31. };
  32. inline auto PreviewPath(int i) {
  33. return u":/gui/emoji/set%1_preview.webp"_q.arg(i);
  34. }
  35. const auto kSets = {
  36. Set{ { 0, 0, 0, "Mac" }, PreviewPath(0) },
  37. Set{ { 1, 2290, 8'306'943, "Android" }, PreviewPath(1) },
  38. Set{ { 2, 2291, 5'694'303, "Twemoji" }, PreviewPath(2) },
  39. Set{ { 3, 2292, 7'261'223, "JoyPixels" }, PreviewPath(3) },
  40. };
  41. using Loading = MTP::DedicatedLoader::Progress;
  42. using SetState = BlobState;
  43. class Loader final : public BlobLoader {
  44. public:
  45. Loader(
  46. not_null<Main::Session*> session,
  47. int id,
  48. MTP::DedicatedLoader::Location location,
  49. const QString &folder,
  50. int size);
  51. void destroy() override;
  52. void unpack(const QString &path) override;
  53. private:
  54. void fail() override;
  55. };
  56. class Inner : public Ui::RpWidget {
  57. public:
  58. Inner(QWidget *parent, not_null<Main::Session*> session);
  59. private:
  60. void setupContent();
  61. const not_null<Main::Session*> _session;
  62. };
  63. class Row : public Ui::RippleButton {
  64. public:
  65. Row(QWidget *widget, not_null<Main::Session*> session, const Set &set);
  66. protected:
  67. void paintEvent(QPaintEvent *e) override;
  68. void onStateChanged(State was, StateChangeSource source) override;
  69. private:
  70. [[nodiscard]] bool showOver() const;
  71. [[nodiscard]] bool showOver(State state) const;
  72. void updateStatusColorOverride();
  73. void setupContent(const Set &set);
  74. void setupLabels(const Set &set);
  75. void setupPreview(const Set &set);
  76. void setupAnimation();
  77. void paintPreview(QPainter &p) const;
  78. void paintRadio(QPainter &p);
  79. void setupHandler();
  80. void load();
  81. void radialAnimationCallback(crl::time now);
  82. void updateLoadingToFinished();
  83. const not_null<Main::Session*> _session;
  84. int _id = 0;
  85. bool _switching = false;
  86. rpl::variable<SetState> _state;
  87. Ui::FlatLabel *_status = nullptr;
  88. std::array<QPixmap, 4> _preview;
  89. Ui::Animations::Simple _toggled;
  90. Ui::Animations::Simple _active;
  91. std::unique_ptr<Ui::RadialAnimation> _loading;
  92. };
  93. base::unique_qptr<Loader> GlobalLoader;
  94. rpl::event_stream<Loader*> GlobalLoaderValues;
  95. void SetGlobalLoader(base::unique_qptr<Loader> loader) {
  96. GlobalLoader = std::move(loader);
  97. GlobalLoaderValues.fire(GlobalLoader.get());
  98. }
  99. int64 GetDownloadSize(int id) {
  100. return ranges::find(kSets, id, &Set::id)->size;
  101. }
  102. [[nodiscard]] float64 CountProgress(not_null<const Loading*> loading) {
  103. return (loading->size > 0)
  104. ? (loading->already / float64(loading->size))
  105. : 0.;
  106. }
  107. MTP::DedicatedLoader::Location GetDownloadLocation(int id) {
  108. const auto username = kCloudLocationUsername.utf16();
  109. const auto i = ranges::find(kSets, id, &Set::id);
  110. return MTP::DedicatedLoader::Location{ username, i->postId };
  111. }
  112. SetState ComputeState(int id) {
  113. if (id == CurrentSetId()) {
  114. return Active();
  115. } else if (SetIsReady(id)) {
  116. return Ready();
  117. }
  118. return Available{ GetDownloadSize(id) };
  119. }
  120. QString StateDescription(const SetState &state) {
  121. return StateDescription(
  122. state,
  123. tr::lng_emoji_set_active);
  124. }
  125. bool GoodSetPartName(const QString &name) {
  126. return (name == u"config.json"_q)
  127. || (name.startsWith(u"emoji_"_q) && name.endsWith(u".webp"_q));
  128. }
  129. bool UnpackSet(const QString &path, const QString &folder) {
  130. return UnpackBlob(path, folder, GoodSetPartName);
  131. }
  132. Loader::Loader(
  133. not_null<Main::Session*> session,
  134. int id,
  135. MTP::DedicatedLoader::Location location,
  136. const QString &folder,
  137. int size)
  138. : BlobLoader(nullptr, session, id, location, folder, size) {
  139. }
  140. void Loader::unpack(const QString &path) {
  141. const auto folder = internal::SetDataPath(id());
  142. const auto weak = Ui::MakeWeak(this);
  143. crl::async([=] {
  144. if (UnpackSet(path, folder)) {
  145. QFile(path).remove();
  146. SwitchToSet(id(), crl::guard(weak, [=](bool success) {
  147. if (success) {
  148. destroy();
  149. } else {
  150. fail();
  151. }
  152. }));
  153. } else {
  154. crl::on_main(weak, [=] {
  155. fail();
  156. });
  157. }
  158. });
  159. }
  160. void Loader::destroy() {
  161. Expects(GlobalLoader == this);
  162. SetGlobalLoader(nullptr);
  163. }
  164. void Loader::fail() {
  165. ClearNeedSwitchToId();
  166. BlobLoader::fail();
  167. }
  168. Inner::Inner(QWidget *parent, not_null<Main::Session*> session)
  169. : RpWidget(parent)
  170. , _session(session) {
  171. setupContent();
  172. }
  173. void Inner::setupContent() {
  174. const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
  175. for (const auto &set : kSets) {
  176. content->add(object_ptr<Row>(content, _session, set));
  177. }
  178. content->resizeToWidth(st::boxWidth);
  179. Ui::ResizeFitChild(this, content);
  180. }
  181. Row::Row(QWidget *widget, not_null<Main::Session*> session, const Set &set)
  182. : RippleButton(widget, st::defaultRippleAnimation)
  183. , _session(session)
  184. , _id(set.id)
  185. , _state(Available{ set.size }) {
  186. setupContent(set);
  187. setupHandler();
  188. }
  189. void Row::paintEvent(QPaintEvent *e) {
  190. auto p = QPainter(this);
  191. const auto over = showOver();
  192. const auto bg = over ? st::windowBgOver : st::windowBg;
  193. p.fillRect(rect(), bg);
  194. paintRipple(p, 0, 0);
  195. paintPreview(p);
  196. paintRadio(p);
  197. }
  198. void Row::paintPreview(QPainter &p) const {
  199. const auto x = st::manageEmojiPreviewPadding.left();
  200. const auto y = st::manageEmojiPreviewPadding.top();
  201. const auto width = st::manageEmojiPreviewWidth;
  202. const auto height = st::manageEmojiPreviewWidth;
  203. auto &&preview = ranges::views::zip(_preview, ranges::views::ints(0, int(_preview.size())));
  204. for (const auto &[pixmap, index] : preview) {
  205. const auto row = (index / 2);
  206. const auto column = (index % 2);
  207. const auto left = x + (column ? width - st::manageEmojiPreview : 0);
  208. const auto top = y + (row ? height - st::manageEmojiPreview : 0);
  209. p.drawPixmap(left, top, pixmap);
  210. }
  211. }
  212. void Row::paintRadio(QPainter &p) {
  213. if (_loading && !_loading->animating()) {
  214. _loading = nullptr;
  215. }
  216. const auto loading = _loading
  217. ? _loading->computeState()
  218. : Ui::RadialState{ 0., 0, arc::kFullLength };
  219. const auto isToggledSet = v::is<Active>(_state.current());
  220. const auto isActiveSet = isToggledSet || v::is<Loading>(_state.current());
  221. const auto toggled = _toggled.value(isToggledSet ? 1. : 0.);
  222. const auto active = _active.value(isActiveSet ? 1. : 0.);
  223. const auto _st = &st::defaultRadio;
  224. PainterHighQualityEnabler hq(p);
  225. const auto left = width()
  226. - st::manageEmojiMarginRight
  227. - _st->diameter
  228. - _st->thickness;
  229. const auto top = (height() - _st->diameter - _st->thickness) / 2;
  230. const auto outerWidth = width();
  231. auto pen = anim::pen(_st->untoggledFg, _st->toggledFg, active);
  232. pen.setWidth(_st->thickness);
  233. pen.setCapStyle(Qt::RoundCap);
  234. p.setPen(pen);
  235. p.setBrush(_st->bg);
  236. const auto rect = style::rtlrect(QRectF(
  237. left,
  238. top,
  239. _st->diameter,
  240. _st->diameter
  241. ).marginsRemoved(QMarginsF(
  242. _st->thickness / 2.,
  243. _st->thickness / 2.,
  244. _st->thickness / 2.,
  245. _st->thickness / 2.
  246. )), outerWidth);
  247. if (loading.shown > 0 && anim::Disabled()) {
  248. anim::DrawStaticLoading(
  249. p,
  250. rect,
  251. _st->thickness,
  252. pen.color(),
  253. _st->bg);
  254. } else if (loading.arcLength < arc::kFullLength) {
  255. p.drawArc(rect, loading.arcFrom, loading.arcLength);
  256. } else {
  257. p.drawEllipse(rect);
  258. }
  259. if (toggled > 0 && (!_loading || !anim::Disabled())) {
  260. p.setPen(Qt::NoPen);
  261. p.setBrush(anim::brush(_st->untoggledFg, _st->toggledFg, toggled));
  262. const auto skip0 = _st->diameter / 2.;
  263. const auto skip1 = _st->skip / 10.;
  264. const auto checkSkip = skip0 * (1. - toggled) + skip1 * toggled;
  265. p.drawEllipse(style::rtlrect(QRectF(
  266. left,
  267. top,
  268. _st->diameter,
  269. _st->diameter
  270. ).marginsRemoved(QMarginsF(
  271. checkSkip,
  272. checkSkip,
  273. checkSkip,
  274. checkSkip
  275. )), outerWidth));
  276. }
  277. }
  278. bool Row::showOver(State state) const {
  279. return (!(state & StateFlag::Disabled))
  280. && (state & (StateFlag::Over | StateFlag::Down));
  281. }
  282. bool Row::showOver() const {
  283. return showOver(state());
  284. }
  285. void Row::onStateChanged(State was, StateChangeSource source) {
  286. RippleButton::onStateChanged(was, source);
  287. if (showOver() != showOver(was)) {
  288. updateStatusColorOverride();
  289. }
  290. }
  291. void Row::updateStatusColorOverride() {
  292. const auto isToggledSet = v::is<Active>(_state.current());
  293. const auto toggled = _toggled.value(isToggledSet ? 1. : 0.);
  294. const auto over = showOver();
  295. if (toggled == 0. && !over) {
  296. _status->setTextColorOverride(std::nullopt);
  297. } else {
  298. _status->setTextColorOverride(anim::color(
  299. over ? st::contactsStatusFgOver : st::contactsStatusFg,
  300. st::contactsStatusFgOnline,
  301. toggled));
  302. }
  303. }
  304. void Row::setupContent(const Set &set) {
  305. _state = GlobalLoaderValues.events_starting_with(
  306. GlobalLoader.get()
  307. ) | rpl::map([=](Loader *loader) {
  308. return (loader && loader->id() == _id)
  309. ? loader->state()
  310. : rpl::single(rpl::empty) | rpl::then(
  311. Updated()
  312. ) | rpl::map([=] {
  313. return ComputeState(_id);
  314. });
  315. }) | rpl::flatten_latest(
  316. ) | rpl::filter([=](const SetState &state) {
  317. return !v::is<Failed>(_state.current())
  318. || !v::is<Available>(state);
  319. });
  320. setupLabels(set);
  321. setupPreview(set);
  322. setupAnimation();
  323. const auto height = st::manageEmojiPreviewPadding.top()
  324. + st::manageEmojiPreviewHeight
  325. + st::manageEmojiPreviewPadding.bottom();
  326. resize(width(), height);
  327. }
  328. void Row::setupHandler() {
  329. clicks(
  330. ) | rpl::filter([=] {
  331. const auto &state = _state.current();
  332. return !_switching && (v::is<Ready>(state)
  333. || v::is<Available>(state));
  334. }) | rpl::start_with_next([=] {
  335. if (v::is<Available>(_state.current())) {
  336. load();
  337. return;
  338. }
  339. _switching = true;
  340. SwitchToSet(_id, crl::guard(this, [=](bool success) {
  341. _switching = false;
  342. if (!success) {
  343. load();
  344. } else if (GlobalLoader && GlobalLoader->id() == _id) {
  345. GlobalLoader->destroy();
  346. }
  347. }));
  348. }, lifetime());
  349. _state.value(
  350. ) | rpl::map([=](const SetState &state) {
  351. return v::is<Ready>(state) || v::is<Available>(state);
  352. }) | rpl::start_with_next([=](bool active) {
  353. setDisabled(!active);
  354. setPointerCursor(active);
  355. }, lifetime());
  356. }
  357. void Row::load() {
  358. LoadAndSwitchTo(_session, _id);
  359. }
  360. void Row::setupLabels(const Set &set) {
  361. using namespace rpl::mappers;
  362. const auto name = Ui::CreateChild<Ui::FlatLabel>(
  363. this,
  364. set.name,
  365. st::localStorageRowTitle);
  366. name->setAttribute(Qt::WA_TransparentForMouseEvents);
  367. _status = Ui::CreateChild<Ui::FlatLabel>(
  368. this,
  369. _state.value() | rpl::map(StateDescription),
  370. st::localStorageRowSize);
  371. _status->setAttribute(Qt::WA_TransparentForMouseEvents);
  372. sizeValue(
  373. ) | rpl::start_with_next([=](QSize size) {
  374. const auto left = st::manageEmojiPreviewPadding.left()
  375. + st::manageEmojiPreviewWidth
  376. + st::manageEmojiPreviewPadding.right();
  377. const auto namey = st::manageEmojiPreviewPadding.top()
  378. + st::manageEmojiNameTop;
  379. const auto statusy = st::manageEmojiPreviewPadding.top()
  380. + st::manageEmojiStatusTop;
  381. name->moveToLeft(left, namey);
  382. _status->moveToLeft(left, statusy);
  383. }, name->lifetime());
  384. }
  385. void Row::setupPreview(const Set &set) {
  386. const auto size = st::manageEmojiPreview * style::DevicePixelRatio();
  387. const auto original = QImage(set.previewPath);
  388. const auto full = original.height();
  389. auto &&preview = ranges::views::zip(_preview, ranges::views::ints(0, int(_preview.size())));
  390. for (auto &&[pixmap, index] : preview) {
  391. pixmap = Ui::PixmapFromImage(original.copy(
  392. { full * index, 0, full, full }
  393. ).scaledToWidth(size, Qt::SmoothTransformation));
  394. pixmap.setDevicePixelRatio(style::DevicePixelRatio());
  395. }
  396. }
  397. void Row::updateLoadingToFinished() {
  398. _loading->update(
  399. v::is<Failed>(_state.current()) ? 0. : 1.,
  400. true,
  401. crl::now());
  402. }
  403. void Row::radialAnimationCallback(crl::time now) {
  404. const auto updated = [&] {
  405. const auto state = _state.current();
  406. if (const auto loading = std::get_if<Loading>(&state)) {
  407. return _loading->update(CountProgress(loading), false, now);
  408. } else {
  409. updateLoadingToFinished();
  410. }
  411. return false;
  412. }();
  413. if (!anim::Disabled() || updated) {
  414. update();
  415. }
  416. }
  417. void Row::setupAnimation() {
  418. using namespace rpl::mappers;
  419. _state.value(
  420. ) | rpl::start_with_next([=](const SetState &state) {
  421. update();
  422. }, lifetime());
  423. _state.value(
  424. ) | rpl::map(
  425. _1 == SetState{ Active() }
  426. ) | rpl::distinct_until_changed(
  427. ) | rpl::start_with_next([=](bool toggled) {
  428. _toggled.start(
  429. [=] { updateStatusColorOverride(); update(); },
  430. toggled ? 0. : 1.,
  431. toggled ? 1. : 0.,
  432. st::defaultRadio.duration);
  433. }, lifetime());
  434. _state.value(
  435. ) | rpl::map([](const SetState &state) {
  436. return v::is<Loading>(state) || v::is<Active>(state);
  437. }) | rpl::distinct_until_changed(
  438. ) | rpl::start_with_next([=](bool active) {
  439. _active.start(
  440. [=] { update(); },
  441. active ? 0. : 1.,
  442. active ? 1. : 0.,
  443. st::defaultRadio.duration);
  444. }, lifetime());
  445. _state.value(
  446. ) | rpl::map([](const SetState &state) {
  447. return std::get_if<Loading>(&state);
  448. }) | rpl::distinct_until_changed(
  449. ) | rpl::start_with_next([=](const Loading *loading) {
  450. if (loading && !_loading) {
  451. _loading = std::make_unique<Ui::RadialAnimation>(
  452. [=](crl::time now) { radialAnimationCallback(now); });
  453. _loading->start(CountProgress(loading));
  454. } else if (!loading && _loading) {
  455. updateLoadingToFinished();
  456. }
  457. }, lifetime());
  458. _toggled.stop();
  459. _active.stop();
  460. updateStatusColorOverride();
  461. }
  462. } // namespace
  463. ManageSetsBox::ManageSetsBox(QWidget*, not_null<Main::Session*> session)
  464. : _session(session) {
  465. }
  466. void ManageSetsBox::prepare() {
  467. const auto inner = setInnerWidget(object_ptr<Inner>(this, _session));
  468. setTitle(tr::lng_emoji_manage_sets());
  469. addButton(tr::lng_close(), [=] { closeBox(); });
  470. setDimensionsToContent(st::boxWidth, inner);
  471. }
  472. void LoadAndSwitchTo(not_null<Main::Session*> session, int id) {
  473. if (!ranges::contains(kSets, id, &Set::id)) {
  474. ClearNeedSwitchToId();
  475. return;
  476. }
  477. SetGlobalLoader(base::make_unique_q<Loader>(
  478. session,
  479. id,
  480. GetDownloadLocation(id),
  481. internal::SetDataPath(id),
  482. GetDownloadSize(id)));
  483. }
  484. } // namespace Emoji
  485. } // namespace Ui