tdesktop/Telegram/SourceFiles/media/stories/media_stories_sibling.cpp
2023-07-25 17:59:53 +04:00

429 lines
11 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 "media/stories/media_stories_sibling.h"
#include "base/weak_ptr.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "media/stories/media_stories_controller.h"
#include "media/stories/media_stories_view.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_player.h"
#include "ui/painter.h"
#include "styles/style_media_view.h"
namespace Media::Stories {
namespace {
constexpr auto kGoodFadeDuration = crl::time(200);
constexpr auto kSiblingFade = 0.5;
constexpr auto kSiblingFadeOver = 0.4;
constexpr auto kSiblingNameOpacity = 0.8;
constexpr auto kSiblingNameOpacityOver = 1.;
constexpr auto kSiblingScaleOver = 0.05;
[[nodiscard]] StoryId LookupShownId(
const Data::StoriesSource &source,
StoryId suggestedId) {
const auto i = suggestedId
? source.ids.lower_bound(Data::StoryIdDates{ suggestedId })
: end(source.ids);
return (i != end(source.ids) && i->id == suggestedId)
? suggestedId
: source.toOpen().id;
}
} // namespace
class Sibling::Loader {
public:
virtual ~Loader() = default;
virtual QImage blurred() = 0;
virtual QImage good() = 0;
};
class Sibling::LoaderPhoto final : public Sibling::Loader {
public:
LoaderPhoto(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> update);
QImage blurred() override;
QImage good() override;
private:
const not_null<PhotoData*> _photo;
const Fn<void()> _update;
std::shared_ptr<Data::PhotoMedia> _media;
rpl::lifetime _waitingLoading;
};
class Sibling::LoaderVideo final
: public Sibling::Loader
, public base::has_weak_ptr {
public:
LoaderVideo(
not_null<DocumentData*> video,
Data::FileOrigin origin,
Fn<void()> update);
QImage blurred() override;
QImage good() override;
private:
void waitForGoodThumbnail();
bool updateAfterGoodCheck();
void createStreamedPlayer();
void streamedFailed();
const not_null<DocumentData*> _video;
const Data::FileOrigin _origin;
const Fn<void()> _update;
std::shared_ptr<Data::DocumentMedia> _media;
std::unique_ptr<Streaming::Instance> _streamed;
rpl::lifetime _waitingGoodGeneration;
bool _checkingGoodInCache = false;
bool _failed = false;
};
Sibling::LoaderPhoto::LoaderPhoto(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> update)
: _photo(photo)
, _update(std::move(update))
, _media(_photo->createMediaView()) {
_photo->load(origin, LoadFromCloudOrLocal, true);
}
QImage Sibling::LoaderPhoto::blurred() {
if (const auto image = _media->thumbnailInline()) {
return image->original();
}
const auto ratio = style::DevicePixelRatio();
auto result = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::black);
result.setDevicePixelRatio(ratio);
return result;
}
QImage Sibling::LoaderPhoto::good() {
if (const auto image = _media->image(Data::PhotoSize::Large)) {
return image->original();
} else if (!_waitingLoading) {
_photo->session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
if (_media->loaded()) {
_update();
}
}, _waitingLoading);
}
return QImage();
}
Sibling::LoaderVideo::LoaderVideo(
not_null<DocumentData*> video,
Data::FileOrigin origin,
Fn<void()> update)
: _video(video)
, _origin(origin)
, _update(std::move( update))
, _media(_video->createMediaView()) {
_media->goodThumbnailWanted();
}
QImage Sibling::LoaderVideo::blurred() {
if (const auto image = _media->thumbnailInline()) {
return image->original();
}
const auto ratio = style::DevicePixelRatio();
auto result = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::black);
result.setDevicePixelRatio(ratio);
return result;
}
QImage Sibling::LoaderVideo::good() {
if (const auto image = _media->goodThumbnail()) {
return image->original();
} else if (!_video->goodThumbnailChecked()
&& !_video->goodThumbnailNoData()) {
if (!_checkingGoodInCache) {
waitForGoodThumbnail();
}
} else if (_failed) {
return QImage();
} else if (!_streamed) {
createStreamedPlayer();
} else if (_streamed->ready()) {
return _streamed->info().video.cover;
}
return QImage();
}
void Sibling::LoaderVideo::createStreamedPlayer() {
_streamed = std::make_unique<Streaming::Instance>(
_video,
_origin,
[] {}); // waitingCallback
_streamed->lockPlayer();
_streamed->player().updates(
) | rpl::start_with_next_error([=](Streaming::Update &&update) {
v::match(update.data, [&](Streaming::Information &update) {
_update();
}, [](const auto &update) {
});
}, [=](Streaming::Error &&error) {
streamedFailed();
}, _streamed->lifetime());
if (_streamed->ready()) {
_update();
} else if (!_streamed->valid()) {
streamedFailed();
} else if (!_streamed->player().active()
&& !_streamed->player().finished()) {
_streamed->play({
.mode = Streaming::Mode::Video,
});
_streamed->pause();
}
}
void Sibling::LoaderVideo::streamedFailed() {
_failed = true;
_streamed = nullptr;
_update();
}
void Sibling::LoaderVideo::waitForGoodThumbnail() {
_checkingGoodInCache = true;
const auto weak = make_weak(this);
_video->owner().cache().get({}, [=](const auto &) {
crl::on_main([=] {
if (const auto strong = weak.get()) {
if (!strong->updateAfterGoodCheck()) {
strong->_video->session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
strong->updateAfterGoodCheck();
}, strong->_waitingGoodGeneration);
}
}
});
});
}
bool Sibling::LoaderVideo::updateAfterGoodCheck() {
if (!_video->goodThumbnailChecked()
&& !_video->goodThumbnailNoData()) {
return false;
}
_checkingGoodInCache = false;
_waitingGoodGeneration.destroy();
_update();
return true;
}
Sibling::Sibling(
not_null<Controller*> controller,
const Data::StoriesSource &source,
StoryId suggestedId)
: _controller(controller)
, _id{ source.user->id, LookupShownId(source, suggestedId) }
, _peer(source.user) {
checkStory();
_goodShown.stop();
}
Sibling::~Sibling() = default;
void Sibling::checkStory() {
const auto maybeStory = _peer->owner().stories().lookup(_id);
if (!maybeStory) {
if (_blurred.isNull()) {
setBlackThumbnail();
if (maybeStory.error() == Data::NoStory::Unknown) {
_peer->owner().stories().resolve(_id, crl::guard(this, [=] {
checkStory();
}));
}
}
return;
}
const auto story = *maybeStory;
const auto origin = Data::FileOrigin();
v::match(story->media().data, [&](not_null<PhotoData*> photo) {
_loader = std::make_unique<LoaderPhoto>(photo, origin, [=] {
check();
});
}, [&](not_null<DocumentData*> document) {
_loader = std::make_unique<LoaderVideo>(document, origin, [=] {
check();
});
}, [&](v::null_t) {
_loader = nullptr;
});
if (!_loader) {
setBlackThumbnail();
return;
}
_blurred = _loader->blurred();
check();
}
void Sibling::setBlackThumbnail() {
_blurred = QImage(
st::storiesMaxSize,
QImage::Format_ARGB32_Premultiplied);
_blurred.fill(Qt::black);
}
FullStoryId Sibling::shownId() const {
return _id;
}
not_null<PeerData*> Sibling::peer() const {
return _peer;
}
bool Sibling::shows(
const Data::StoriesSource &source,
StoryId suggestedId) const {
const auto fullId = FullStoryId{
source.user->id,
LookupShownId(source, suggestedId),
};
return (_id == fullId);
}
SiblingView Sibling::view(const SiblingLayout &layout, float64 over) {
const auto name = nameImage(layout);
return {
.image = _good.isNull() ? _blurred : _good,
.layout = {
.geometry = layout.geometry,
.fade = kSiblingFade * (1 - over) + kSiblingFadeOver * over,
.radius = st::storiesRadius,
},
.userpic = userpicImage(layout),
.userpicPosition = layout.userpic.topLeft(),
.name = name,
.namePosition = namePosition(layout, name),
.nameOpacity = (kSiblingNameOpacity * (1 - over)
+ kSiblingNameOpacityOver * over),
.scale = 1. + (over * kSiblingScaleOver),
};
}
QImage Sibling::userpicImage(const SiblingLayout &layout) {
const auto ratio = style::DevicePixelRatio();
const auto size = layout.userpic.width() * ratio;
const auto key = _peer->userpicUniqueKey(_userpicView);
if (_userpicImage.width() != size || _userpicKey != key) {
_userpicKey = key;
_userpicImage = _peer->generateUserpicImage(_userpicView, size);
_userpicImage.setDevicePixelRatio(ratio);
}
return _userpicImage;
}
QImage Sibling::nameImage(const SiblingLayout &layout) {
if (_nameFontSize != layout.nameFontSize) {
_nameFontSize = layout.nameFontSize;
const auto family = 0; // Default font family.
const auto font = style::font(
_nameFontSize,
style::internal::FontSemibold,
family);
_name.reset();
_nameStyle = std::make_unique<style::TextStyle>(style::TextStyle{
.font = font,
.linkFont = font,
.linkFontOver = font,
});
};
const auto text = _peer->isSelf()
? tr::lng_stories_my_name(tr::now)
: _peer->shortName();
if (_nameText != text) {
_name.reset();
_nameText = text;
}
if (!_name) {
_nameAvailableWidth = 0;
_name.emplace(*_nameStyle, _nameText);
}
const auto available = layout.nameBoundingRect.width();
const auto wasCut = (_nameAvailableWidth < _name->maxWidth());
const auto nowCut = (available < _name->maxWidth());
if (_nameImage.isNull()
|| _nameAvailableWidth != layout.nameBoundingRect.width()) {
_nameAvailableWidth = layout.nameBoundingRect.width();
if (_nameImage.isNull() || nowCut || wasCut) {
const auto w = std::min(_nameAvailableWidth, _name->maxWidth());
const auto h = _nameStyle->font->height;
const auto ratio = style::DevicePixelRatio();
_nameImage = QImage(
QSize(w, h) * ratio,
QImage::Format_ARGB32_Premultiplied);
_nameImage.setDevicePixelRatio(ratio);
_nameImage.fill(Qt::transparent);
auto p = Painter(&_nameImage);
auto hq = PainterHighQualityEnabler(p);
p.setFont(_nameStyle->font);
p.setPen(Qt::white);
_name->drawLeftElided(p, 0, 0, w, w);
}
}
return _nameImage;
}
QPoint Sibling::namePosition(
const SiblingLayout &layout,
const QImage &image) const {
const auto size = image.size() / image.devicePixelRatio();
const auto width = size.width();
const auto bounding = layout.nameBoundingRect;
const auto left = layout.geometry.x()
+ (layout.geometry.width() - width) / 2;
const auto top = bounding.y() + bounding.height() - size.height();
if (left < bounding.x()) {
return { bounding.x(), top };
} else if (left + width > bounding.x() + bounding.width()) {
return { bounding.x() + bounding.width() - width, top };
}
return { left, top };
}
void Sibling::check() {
Expects(_loader != nullptr);
auto good = _loader->good();
if (good.isNull()) {
return;
}
_loader = nullptr;
_good = std::move(good);
_goodShown.start([=] {
_controller->repaintSibling(this);
}, 0., 1., kGoodFadeDuration, anim::linear);
}
} // namespace Media::Stories