Implement animated stickerset thumbnails.

This commit is contained in:
John Preston 2019-07-02 13:46:37 +02:00
parent db2d24ff32
commit 3b645422ff
11 changed files with 381 additions and 63 deletions

View File

@ -1093,6 +1093,36 @@ RecentStickerPack &GetRecentPack() {
return cRefRecentStickers();
}
template <typename Method>
auto LottieCachedFromContent(
Method &&method,
Storage::Cache::Key baseKey,
LottieSize sizeTag,
not_null<AuthSession*> session,
const QByteArray &content,
QSize box) {
const auto key = Storage::Cache::Key{
baseKey.high,
baseKey.low + int(sizeTag)
};
const auto get = [=](FnMut<void(QByteArray &&cached)> handler) {
session->data().cacheBigFile().get(
key,
std::move(handler));
};
const auto weak = base::make_weak(session.get());
const auto put = [=](QByteArray &&cached) {
crl::on_main(weak, [=, data = std::move(cached)]() mutable {
weak->data().cacheBigFile().put(key, std::move(data));
});
};
return method(
get,
put,
content,
Lottie::FrameRequest{ box });
}
template <typename Method>
auto LottieFromDocument(
Method &&method,
@ -1108,26 +1138,13 @@ auto LottieFromDocument(
Lottie::FrameRequest{ box });
}
if (const auto baseKey = document->bigFileBaseCacheKey()) {
const auto key = Storage::Cache::Key{
baseKey->high,
baseKey->low + int(sizeTag)
};
const auto get = [=](FnMut<void(QByteArray &&cached)> handler) {
document->session().data().cacheBigFile().get(
key,
std::move(handler));
};
const auto weak = base::make_weak(&document->session());
const auto put = [=](QByteArray &&cached) {
crl::on_main(weak, [=, data = std::move(cached)]() mutable {
weak->data().cacheBigFile().put(key, std::move(data));
});
};
return method(
get,
put,
return LottieCachedFromContent(
std::forward<Method>(method),
*baseKey,
sizeTag,
&document->session(),
Lottie::ReadContent(data, filepath),
Lottie::FrameRequest{ box });
box);
}
return method(
Lottie::ReadContent(data, filepath),
@ -1156,4 +1173,98 @@ not_null<Lottie::Animation*> LottieAnimationFromDocument(
return LottieFromDocument(method, document, sizeTag, box);
}
bool HasLottieThumbnail(
ImagePtr thumbnail,
not_null<DocumentData*> sticker) {
if (thumbnail) {
if (!thumbnail->loaded()) {
return false;
}
const auto &location = thumbnail->location();
const auto &bytes = thumbnail->bytesForCache();
return location.valid()
&& location.type() == StorageFileLocation::Type::StickerSetThumb
&& !bytes.isEmpty();
} else if (const auto info = sticker->sticker()) {
if (!info->animated) {
return false;
}
sticker->automaticLoad(sticker->stickerSetOrigin(), nullptr);
if (!sticker->loaded()) {
return false;
}
return sticker->bigFileBaseCacheKey().has_value();
}
return false;
}
std::unique_ptr<Lottie::SinglePlayer> LottieThumbnail(
ImagePtr thumbnail,
not_null<DocumentData*> sticker,
LottieSize sizeTag,
QSize box,
std::shared_ptr<Lottie::FrameRenderer> renderer) {
const auto baseKey = thumbnail
? thumbnail->location().file().bigFileBaseCacheKey()
: sticker->bigFileBaseCacheKey();
if (!baseKey) {
return nullptr;
}
const auto content = (thumbnail
? thumbnail->bytesForCache()
: Lottie::ReadContent(sticker->data(), sticker->filepath()));
if (content.isEmpty()) {
return nullptr;
}
const auto method = [](auto &&...args) {
return std::make_unique<Lottie::SinglePlayer>(
std::forward<decltype(args)>(args)...);
};
return LottieCachedFromContent(
method,
*baseKey,
sizeTag,
&sticker->session(),
content,
box);
}
ThumbnailSource::ThumbnailSource(
const StorageImageLocation &location,
int size)
: StorageSource(location, size) {
}
QImage ThumbnailSource::takeLoaded() {
if (_bytesForAnimated.isEmpty()
&& _loader
&& _loader->finished()
&& !_loader->cancelled()) {
_bytesForAnimated = _loader->bytes();
}
auto result = StorageSource::takeLoaded();
if (!_bytesForAnimated.isEmpty()
&& !result.isNull()
&& result.size() != Image::Empty()->original().size()) {
_bytesForAnimated = QByteArray();
}
return result;
}
QByteArray ThumbnailSource::bytesForCache() {
return _bytesForAnimated;
}
std::unique_ptr<FileLoader> ThumbnailSource::createLoader(
Data::FileOrigin origin,
LoadFromCloudSetting fromCloud,
bool autoLoading) {
auto result = StorageSource::createLoader(
origin,
fromCloud,
autoLoading);
_loader = result.get();
return result;
}
} // namespace Stickers

View File

@ -8,12 +8,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "mtproto/sender.h"
#include "ui/image/image_source.h"
class DocumentData;
class AuthSession;
namespace Storage {
namespace Cache {
struct Key;
} // namespace Cache
} // namespace Storage
namespace Lottie {
class SinglePlayer;
class MultiPlayer;
class FrameRenderer;
class Animation;
} // namespace Lottie
@ -115,16 +124,50 @@ enum class LottieSize : uchar {
MessageHistory,
StickerSet,
StickersPanel,
StickersFooter,
SetsListThumbnail,
};
std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
not_null<DocumentData*> document,
LottieSize sizeTag,
QSize box);
not_null<Lottie::Animation*> LottieAnimationFromDocument(
[[nodiscard]] not_null<Lottie::Animation*> LottieAnimationFromDocument(
not_null<Lottie::MultiPlayer*> player,
not_null<DocumentData*> document,
LottieSize sizeTag,
QSize box);
[[nodiscard]] bool HasLottieThumbnail(
ImagePtr thumbnail,
not_null<DocumentData*> sticker);
[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottieThumbnail(
ImagePtr thumbnail,
not_null<DocumentData*> sticker,
LottieSize sizeTag,
QSize box,
std::shared_ptr<Lottie::FrameRenderer> renderer = nullptr);
class ThumbnailSource : public Images::StorageSource {
public:
ThumbnailSource(
const StorageImageLocation &location,
int size);
QImage takeLoaded() override;
QByteArray bytesForCache() override;
protected:
std::unique_ptr<FileLoader> createLoader(
Data::FileOrigin origin,
LoadFromCloudSetting fromCloud,
bool autoLoading) override;
private:
QPointer<FileLoader> _loader;
QByteArray _bytesForAnimated;
};
} // namespace Stickers

View File

@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/ripple_animation.h"
#include "ui/image/image.h"
#include "lottie/lottie_multi_player.h"
#include "lottie/lottie_single_player.h"
#include "lottie/lottie_animation.h"
#include "boxes/stickers_box.h"
#include "inline_bots/inline_bot_result.h"
@ -64,6 +65,7 @@ struct StickerIcon {
}
uint64 setId = 0;
ImagePtr thumbnail;
mutable Lottie::SinglePlayer *lottie = nullptr;
DocumentData *sticker = nullptr;
ChannelData *megagroup = nullptr;
int pixw = 0;
@ -71,9 +73,11 @@ struct StickerIcon {
};
class StickersListWidget::Footer : public TabbedSelector::InnerFooter, private base::Subscriber {
class StickersListWidget::Footer
: public TabbedSelector::InnerFooter
, private base::Subscriber {
public:
Footer(not_null<StickersListWidget*> parent);
explicit Footer(not_null<StickersListWidget*> parent);
void preloadImages();
void validateSelectedIcon(
@ -88,6 +92,8 @@ public:
void returnFocus();
void setLoading(bool loading);
void clearLottieData();
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
@ -106,6 +112,12 @@ private:
};
using OverState = base::variant<SpecialOver, int>;
struct LottieIcon {
std::unique_ptr<Lottie::SinglePlayer> player;
bool stale = false;
rpl::lifetime lifetime;
};
template <typename Callback>
void enumerateVisibleIcons(Callback callback);
@ -113,9 +125,13 @@ private:
void setSelectedIcon(
int newSelected,
ValidateIconAnimations animations);
void validateIconLottieAnimation(const StickerIcon &icon);
QSize iconBox() const;
void refreshIconsGeometry(ValidateIconAnimations animations);
void refillLottieData();
void updateSelected();
void updateSetIcon(uint64 setId);
void finishDragging();
void paintStickerSettingsIcon(Painter &p) const;
void paintSearchIcon(Painter &p) const;
@ -134,6 +150,7 @@ private:
static constexpr auto kVisibleIconsCount = 8;
QList<StickerIcon> _icons;
mutable base::flat_map<uint64, LottieIcon> _lottieData;
OverState _iconOver = SpecialOver::None;
int _iconSel = 0;
OverState _iconDown = SpecialOver::None;
@ -191,7 +208,8 @@ StickersListWidget::Set &StickersListWidget::Set::operator=(
Set &&other) = default;
StickersListWidget::Set::~Set() = default;
StickersListWidget::Footer::Footer(not_null<StickersListWidget*> parent) : InnerFooter(parent)
StickersListWidget::Footer::Footer(not_null<StickersListWidget*> parent)
: InnerFooter(parent)
, _pan(parent)
, _iconsAnimation([=](crl::time now) {
return iconsAnimationCallback(now);
@ -205,6 +223,34 @@ StickersListWidget::Footer::Footer(not_null<StickersListWidget*> parent) : Inner
});
}
void StickersListWidget::Footer::clearLottieData() {
for (auto &icon : _icons) {
icon.lottie = nullptr;
}
_lottieData.clear();
}
void StickersListWidget::Footer::refillLottieData() {
for (auto &item : _lottieData) {
item.second.stale = true;
}
for (auto &icon : _icons) {
const auto i = _lottieData.find(icon.setId);
if (i == end(_lottieData)) {
continue;
}
icon.lottie = i->second.player.get();
i->second.stale = false;
}
for (auto i = begin(_lottieData); i != end(_lottieData);) {
if (i->second.stale) {
i = _lottieData.erase(i);
} else {
++i;
}
}
}
void StickersListWidget::Footer::initSearch() {
_searchField.create(
this,
@ -282,7 +328,7 @@ void StickersListWidget::Footer::enumerateVisibleIcons(Callback callback) {
}
void StickersListWidget::Footer::preloadImages() {
enumerateVisibleIcons([](const StickerIcon & icon, int x) {
enumerateVisibleIcons([](const StickerIcon &icon, int x) {
if (const auto sticker = icon.sticker) {
const auto origin = sticker->stickerSetOrigin();
if (icon.thumbnail) {
@ -607,6 +653,7 @@ void StickersListWidget::Footer::updateSelected() {
void StickersListWidget::Footer::refreshIcons(
ValidateIconAnimations animations) {
_pan->fillIcons(_icons);
refillLottieData();
refreshIconsGeometry(animations);
}
@ -654,6 +701,49 @@ void StickersListWidget::Footer::paintFeaturedStickerSetsBadge(Painter &p, int i
}
}
QSize StickersListWidget::Footer::iconBox() const {
return QSize(
st::stickerIconWidth - 2 * st::stickerIconPadding,
st::emojiFooterHeight - 2 * st::stickerIconPadding);
}
void StickersListWidget::Footer::validateIconLottieAnimation(
const StickerIcon &icon) {
if (icon.lottie
|| !Stickers::HasLottieThumbnail(ImagePtr(), icon.sticker)) {
return;
}
auto player = Stickers::LottieThumbnail(
ImagePtr(),
icon.sticker,
Stickers::LottieSize::StickersFooter,
iconBox() * cIntRetinaFactor(),
_pan->getLottieRenderer());
if (!player) {
return;
}
icon.lottie = player.get();
const auto id = icon.setId;
const auto [i, ok] = _lottieData.emplace(
id,
LottieIcon{ std::move(player) });
Assert(ok);
icon.lottie->updates(
) | rpl::start_with_next([=] {
updateSetIcon(id);
}, i->second.lifetime);
}
void StickersListWidget::Footer::updateSetIcon(uint64 setId) {
enumerateVisibleIcons([&](const StickerIcon &icon, int x) {
if (icon.setId != setId) {
return;
}
update(x, _iconsTop, st::stickerIconWidth, st::emojiFooterHeight);
});
}
void StickersListWidget::Footer::paintSetIcon(
Painter &p,
const StickerIcon &icon,
@ -663,11 +753,35 @@ void StickersListWidget::Footer::paintSetIcon(
const auto thumb = icon.thumbnail
? icon.thumbnail.get()
: icon.sticker->thumbnail();
if (thumb) {
thumb->load(origin);
if (!thumb) {
return;
}
thumb->load(origin);
if (!thumb->loaded()) {
return;
}
const_cast<Footer*>(this)->validateIconLottieAnimation(icon);
if (!icon.lottie) {
auto pix = thumb->pix(origin, icon.pixw, icon.pixh);
p.drawPixmapLeft(x + (st::stickerIconWidth - icon.pixw) / 2, _iconsTop + (st::emojiFooterHeight - icon.pixh) / 2, width(), pix);
} else if (icon.lottie->ready()) {
auto request = Lottie::FrameRequest();
request.box = iconBox() * cIntRetinaFactor();
const auto frame = icon.lottie->frame(request);
const auto size = frame.size() / cIntRetinaFactor();
p.drawImage(
QRect(
x + (st::stickerIconWidth - size.width()) / 2,
_iconsTop + (st::emojiFooterHeight - size.height()) / 2,
size.width(),
size.height()),
frame);
const auto paused = _pan->controller()->isGifPausedAtLeastFor(
Window::GifPauseReason::SavedGifs);
if (!paused) {
icon.lottie->markFrameShown();
}
}
} else if (icon.megagroup) {
icon.megagroup->paintUserpicLeft(p, x + (st::stickerIconWidth - st::stickerGroupCategorySize) / 2, _iconsTop + (st::emojiFooterHeight - st::stickerGroupCategorySize) / 2, width(), st::stickerGroupCategorySize);
@ -1901,11 +2015,17 @@ TabbedSelector::InnerFooter *StickersListWidget::getFooter() const {
void StickersListWidget::processHideFinished() {
clearSelection();
clearLottieData();
if (_footer) {
_footer->clearLottieData();
}
}
void StickersListWidget::processPanelHideFinished() {
clearInstalledLocally();
clearLottieData();
if (_footer) {
_footer->clearLottieData();
}
// Preserve panel state through visibility toggles.
//// Reset to the recent stickers section.
//if (_section == Section::Featured && (!_footer || !_footer->hasOnlyFeaturedSets())) {

View File

@ -69,6 +69,8 @@ public:
void sendSearchRequest();
void searchForSets(const QString &query);
std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
~StickersListWidget();
protected:
@ -290,8 +292,6 @@ private:
void showPreview();
std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
ChannelData *_megagroupSet = nullptr;
uint64 _megagroupSetIdRequested = 0;
std::vector<Set> _mySets;

View File

@ -214,6 +214,10 @@ class TabbedSelector::Inner : public Ui::RpWidget {
public:
Inner(QWidget *parent, not_null<Window::SessionController*> controller);
not_null<Window::SessionController*> controller() const {
return _controller;
}
int getVisibleTop() const {
return _visibleTop;
}
@ -246,10 +250,6 @@ protected:
int minimalHeight() const;
int resizeGetHeight(int newWidth) override final;
not_null<Window::SessionController*> controller() const {
return _controller;
}
virtual int countDesiredHeight(int newWidth) = 0;
virtual InnerFooter *getFooter() const = 0;
virtual void processHideFinished() {

View File

@ -13,20 +13,22 @@ namespace Lottie {
SinglePlayer::SinglePlayer(
const QByteArray &content,
const FrameRequest &request)
const FrameRequest &request,
std::shared_ptr<FrameRenderer> renderer)
: _animation(this, content, request)
, _timer([=] { checkNextFrameRender(); })
, _renderer(FrameRenderer::Instance()) {
, _renderer(renderer ? renderer : FrameRenderer::Instance()) {
}
SinglePlayer::SinglePlayer(
FnMut<void(FnMut<void(QByteArray &&cached)>)> get, // Main thread.
FnMut<void(QByteArray &&cached)> put, // Unknown thread.
const QByteArray &content,
const FrameRequest &request)
const FrameRequest &request,
std::shared_ptr<FrameRenderer> renderer)
: _animation(this, std::move(get), std::move(put), content, request)
, _timer([=] { checkNextFrameRender(); })
, _renderer(FrameRenderer::Instance()) {
, _renderer(renderer ? renderer : FrameRenderer::Instance()) {
}
SinglePlayer::~SinglePlayer() {

View File

@ -30,12 +30,14 @@ class SinglePlayer final : public Player {
public:
SinglePlayer(
const QByteArray &content,
const FrameRequest &request);
const FrameRequest &request,
std::shared_ptr<FrameRenderer> renderer = nullptr);
SinglePlayer(
FnMut<void(FnMut<void(QByteArray &&cached)>)> get, // Main thread.
FnMut<void(QByteArray &&cached)> put, // Unknown thread.
const QByteArray &content,
const FrameRequest &request);
const FrameRequest &request,
std::shared_ptr<FrameRenderer> renderer = nullptr);
~SinglePlayer();
void start(

View File

@ -3516,7 +3516,7 @@ void _readStickerSets(FileKey &stickersKey, Stickers::Order *outOrder = nullptr,
setHash,
MTPDstickerSet::Flags(setFlags),
setInstallDate,
Images::Create(setThumbnail)));
Images::CreateStickerSetThumbnail(setThumbnail)));
}
auto &set = it.value();
auto inputSet = MTP_inputStickerSetID(MTP_long(set.id), MTP_long(set.access));

View File

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "storage/cache/storage_cache_database.h"
#include "data/data_session.h"
#include "data/data_file_origin.h"
#include "chat_helpers/stickers.h"
#include "auth_session.h"
using namespace Images;
@ -161,7 +162,11 @@ ImagePtr Create(int width, int height) {
height)));
}
ImagePtr Create(const StorageImageLocation &location, int size) {
template <typename SourceType>
ImagePtr Create(
const StorageImageLocation &location,
int size,
const QByteArray &bytes) {
if (!location.valid()) {
return ImagePtr();
}
@ -173,41 +178,63 @@ ImagePtr Create(const StorageImageLocation &location, int size) {
: StorageImages.emplace(
key,
std::make_unique<Image>(
std::make_unique<StorageSource>(location, size))
std::make_unique<SourceType>(location, size))
).first->second.get();
if (found) {
image->refreshFileReference(location.fileReference());
}
if (!bytes.isEmpty()) {
image->setImageBytes(bytes);
}
return ImagePtr(image);
}
ImagePtr Create(const StorageImageLocation &location, int size) {
return Create<StorageSource>(location, size, QByteArray());
}
ImagePtr Create(
const StorageImageLocation &location,
const QByteArray &bytes) {
const auto key = inMemoryKey(location);
const auto i = StorageImages.find(key);
const auto found = (i != end(StorageImages));
const auto image = found
? i->second.get()
: StorageImages.emplace(
key,
std::make_unique<Image>(
std::make_unique<StorageSource>(location, bytes.size()))
).first->second.get();
if (found) {
image->refreshFileReference(location.fileReference());
}
image->setImageBytes(bytes);
return ImagePtr(image);
return Create<StorageSource>(location, bytes.size(), bytes);
}
template <typename CreateLocation>
struct CreateStorageImage {
ImagePtr operator()(
const StorageImageLocation &location,
int size) {
return Create(location, size);
}
ImagePtr operator()(
const StorageImageLocation &location,
const QByteArray &bytes) {
return Create(location, bytes);
}
};
struct CreateSetThumbnail {
using Source = Stickers::ThumbnailSource;
ImagePtr operator()(
const StorageImageLocation &location,
int size) {
return Create<Source>(location, size, QByteArray());
}
ImagePtr operator()(
const StorageImageLocation &location,
const QByteArray &bytes) {
return Create<Source>(location, bytes.size(), bytes);
}
};
template <typename CreateLocation, typename Method = CreateStorageImage>
ImagePtr CreateFromPhotoSize(
CreateLocation &&createLocation,
const MTPPhotoSize &size) {
const MTPPhotoSize &size,
Method method = Method()) {
return size.match([&](const MTPDphotoSize &data) {
const auto &location = data.vlocation.c_fileLocationToBeDeprecated();
return Create(
return method(
StorageImageLocation(
createLocation(data.vtype, location),
data.vw.v,
@ -216,7 +243,7 @@ ImagePtr CreateFromPhotoSize(
}, [&](const MTPDphotoCachedSize &data) {
const auto bytes = qba(data.vbytes);
const auto &location = data.vlocation.c_fileLocationToBeDeprecated();
return Create(
return method(
StorageImageLocation(
createLocation(data.vtype, location),
data.vw.v,
@ -291,7 +318,11 @@ ImagePtr Create(const MTPDstickerSet &set, const MTPPhotoSize &size) {
location.vvolume_id,
location.vlocal_id));
};
return CreateFromPhotoSize(create, size);
return CreateFromPhotoSize(create, size, CreateSetThumbnail());
}
ImagePtr CreateStickerSetThumbnail(const StorageImageLocation &location) {
return CreateSetThumbnail()(location, 0);
}
ImagePtr Create(const MTPDphoto &photo, const MTPPhotoSize &size) {

View File

@ -27,6 +27,7 @@ ImagePtr Create(
QImage &&data);
ImagePtr Create(int width, int height);
ImagePtr Create(const StorageImageLocation &location, int size = 0);
ImagePtr CreateStickerSetThumbnail(const StorageImageLocation &location);
ImagePtr Create( // photoCachedSize
const StorageImageLocation &location,
const QByteArray &bytes);

View File

@ -407,9 +407,17 @@ Storage::Cache::Key StorageFileLocation::bigFileBaseCacheKey() const {
return Storage::Cache::Key{ high, low };
}
case Type::StickerSetThumb: {
const auto high = (uint64(uint32(_localId)) << 24)
| ((uint64(_type) + 1) << 16)
| ((uint64(_dcId) & 0xFFULL) << 8)
| (_volumeId >> 56);
const auto low = (_volumeId << 8);
return Storage::Cache::Key{ high, low };
}
case Type::Legacy:
case Type::PeerPhoto:
case Type::StickerSetThumb:
case Type::Encrypted:
case Type::Secure:
case Type::Photo: