Support albums layout.

This commit is contained in:
John Preston 2024-02-13 11:53:40 +04:00
parent 7755b70317
commit f9069144e5
6 changed files with 431 additions and 72 deletions

View file

@ -499,7 +499,7 @@ img,
video,
iframe {
max-width: 100%;
max-height: 400px;
max-height: 480px;
vertical-align: top;
}
video {
@ -703,24 +703,37 @@ figure.slideshow > figure figcaption a:hover {
opacity: 1;
}
figure.collage-wrap {
margin: 0px 12px;
}
figure.collage-wrap figcaption {
padding: 6px 6px 0px;
}
figure.collage {
margin: -2px 16px;
text-align: left;
overflow: hidden;
border-radius: 6px;
}
figure.collage > figure {
display: inline-block;
vertical-align: top;
width: calc(25% - 4px);
margin: 2px;
box-sizing: border-box;
figure.collage .photo-wrap,
figure.collage .video-wrap {
position: absolute;
}
figure.collage > figure > i {
background: no-repeat center;
figure.collage .photo-wrap .photo {
background-size: cover;
display: inline-block;
vertical-align: top;
width: 100%;
padding-top: 100%;
}
figure.collage .video-wrap video {
object-fit: cover;
position: absolute;
top: 50%;
left: 50%;
width: auto;
height: auto;
min-width: 100%;
min-height: 100%;
transform: translate(-50%, -50%);
}
figure.collage .video-wrap .video-small,
video[autoplay] {
pointer-events: none;
}
figure.table-wrap {
@ -983,13 +996,74 @@ section.channel > a > h4 {
display: block;
margin: 0 auto;
}
.photo-wrap {
.photo-wrap,
.video-wrap {
width: 100%;
background-size: 100%;
margin: 0 auto;
position: relative;
overflow: hidden;
}
.photo-bg,
.video-bg {
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: absolute;
filter: blur(16px);
width: 100%;
height: 100%;
}
.video-bg,
video {
position: absolute;
top: 0px;
}
.photo {
background-size: 100%;
position: relative;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}
.photo,
video {
opacity: 0;
transition: opacity 300ms ease-in-out;
}
.photo.loaded,
video.loaded {
opacity: 1;
}
.video-play-outer {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.video-play {
position: relative;
width: 48px;
height: 0;
padding-top: 48px;
max-width: 48px;
max-height: 48px;
background-color: rgba(0, 0, 0, 0.34);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.video-play::before {
content: '';
position: absolute;
margin: -48px -4px 0px 0px;
width: 0;
height: 0;
border-style: solid;
border-width: 10px 0 10px 16px;
border-color: transparent transparent transparent white;
}
.toast {

View file

@ -226,6 +226,36 @@ var IV = {
IV.stopRipples(e.currentTarget);
});
}
const photos = document.getElementsByClassName('photo');
for (let i = 0; i < photos.length; ++i) {
const photo = photos[i];
if (photo.classList.contains('loaded')) {
continue;
}
const url = photo.style.backgroundImage;
if (!url || url.length < 7) {
continue;
}
var img = new Image();
img.onload = function () {
photo.classList.add('loaded');
}
img.src = url.substr(5, url.length - 7);
if (img.complete) {
img.onload();
}
}
const videos = document.getElementsByTagName('video');
for (let i = 0; i < videos.length; ++i) {
const video = videos[i];
if (video.classList.contains('loaded')) {
continue;
}
video.addEventListener('canplay', function () {
video.classList.add('loaded');
});
}
IV.notify({ event: 'ready' });
},
showTooltip: function (text) {

View file

@ -489,6 +489,7 @@ void Controller::processLink(const QString &url, const QString &context) {
const auto channelPrefix = u"channel"_q;
const auto joinPrefix = u"join_link"_q;
const auto webpagePrefix = u"webpage"_q;
const auto viewerPrefix = u"viewer"_q;
if (context.startsWith(channelPrefix)) {
_events.fire({
.type = Event::Type::OpenChannel,
@ -505,6 +506,12 @@ void Controller::processLink(const QString &url, const QString &context) {
.url = url,
.context = context.mid(webpagePrefix.size()),
});
} else if (context.startsWith(viewerPrefix)) {
_events.fire({
.type = Event::Type::OpenMedia,
.url = url,
.context = context.mid(viewerPrefix.size()),
});
} else if (context.isEmpty()) {
_events.fire({ .type = Event::Type::OpenLink, .url = url });
}

View file

@ -43,6 +43,7 @@ public:
OpenPage,
OpenLink,
OpenLinkExternal,
OpenMedia,
};
Type type = Type::Close;
QString url;

View file

@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h"
#include "main/session/session_show.h"
#include "media/streaming/media_streaming_loader.h"
#include "media/view/media_view_open_common.h"
#include "storage/file_download.h"
#include "storage/storage_domain.h"
#include "ui/boxes/confirm_box.h"
@ -796,6 +797,40 @@ void Instance::show(
case Type::OpenLinkExternal:
QDesktopServices::openUrl(event.url);
break;
case Type::OpenMedia:
if (const auto window = Core::App().activeWindow()) {
const auto current = window->sessionController();
const auto controller = (current
&& &current->session() == _shownSession)
? current
: nullptr;
const auto item = (HistoryItem*)nullptr;
const auto topicRootId = MsgId(0);
if (event.context.startsWith("-photo")) {
const auto id = event.context.mid(6).toULongLong();
const auto photo = _shownSession->data().photo(id);
if (!photo->isNull()) {
window->openInMediaView({
controller,
photo,
item,
topicRootId
});
}
} else if (event.context.startsWith("-video")) {
const auto id = event.context.mid(6).toULongLong();
const auto video = _shownSession->data().document(id);
if (!video->isNull()) {
window->openInMediaView({
controller,
video,
item,
topicRootId
});
}
}
}
break;
case Type::OpenPage:
case Type::OpenLink:
_shownSession->api().request(MTPmessages_GetWebPage(

View file

@ -12,7 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "iv/iv_data.h"
#include "lang/lang_keys.h"
#include "ui/image/image_prepare.h"
#include "ui/grouped_layout.h"
#include "styles/palette.h"
#include "styles/style_chat.h"
#include <QtCore/QSize>
@ -34,6 +36,19 @@ struct Photo {
struct Document {
uint64 id = 0;
int width = 0;
int height = 0;
QByteArray minithumbnail;
};
template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
[[nodiscard]] QByteArray Number(T value) {
return QByteArray::number(value);
}
template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
[[nodiscard]] QByteArray Percent(T value) {
return Number(base::SafeRound(value * 10000.) / 100.);
};
class Parser final {
@ -50,6 +65,11 @@ private:
template <typename Inner>
[[nodiscard]] QByteArray list(const MTPVector<Inner> &data);
[[nodiscard]] QByteArray collage(
const QVector<MTPPageBlock> &list,
const std::vector<QSize> &dimensions,
int offset = 0);
[[nodiscard]] QByteArray block(const MTPDpageBlockUnsupported &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockTitle &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockSubtitle &data);
@ -64,8 +84,14 @@ private:
[[nodiscard]] QByteArray block(const MTPDpageBlockList &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockBlockquote &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockPullquote &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockPhoto &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockVideo &data);
[[nodiscard]] QByteArray block(
const MTPDpageBlockPhoto &data,
const Ui::GroupMediaLayout &layout = {},
QSize outer = {});
[[nodiscard]] QByteArray block(
const MTPDpageBlockVideo &data,
const Ui::GroupMediaLayout &layout = {},
QSize outer = {});
[[nodiscard]] QByteArray block(const MTPDpageBlockCover &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockEmbed &data);
[[nodiscard]] QByteArray block(const MTPDpageBlockEmbedPost &data);
@ -123,6 +149,9 @@ private:
int zoom);
[[nodiscard]] QByteArray resource(QByteArray id);
[[nodiscard]] std::vector<QSize> computeCollageDimensions(
const QVector<MTPPageBlock> &items);
const Options _options;
base::flat_set<QByteArray> _resources;
@ -209,6 +238,60 @@ QByteArray Parser::list(const MTPVector<Inner> &data) {
return result.join(QByteArray());
}
QByteArray Parser::collage(
const QVector<MTPPageBlock> &list,
const std::vector<QSize> &dimensions,
int offset) {
Expects(list.size() == dimensions.size());
constexpr auto kPerCollage = 10;
const auto last = (offset + kPerCollage >= int(dimensions.size()));
auto result = QByteArray();
auto slice = ((offset > 0) || (dimensions.size() > kPerCollage))
? (dimensions
| ranges::views::drop(offset)
| ranges::views::take(kPerCollage)
| ranges::to_vector)
: dimensions;
const auto layout = Ui::LayoutMediaGroup(
slice,
st::historyGroupWidthMax,
st::historyGroupWidthMin,
st::historyGroupSkip);
auto size = QSize();
for (const auto &part : layout) {
const auto &rect = part.geometry;
size = QSize(
std::max(size.width(), rect.x() + rect.width()),
std::max(size.height(), rect.y() + rect.height()));
}
for (auto i = 0, count = int(layout.size()); i != count; ++i) {
const auto &part = layout[i];
list[offset + i].match([&](const MTPDpageBlockPhoto &data) {
result += block(data, part, size);
}, [&](const MTPDpageBlockVideo &data) {
result += block(data, part, size);
}, [](const auto &) {
Unexpected("Block type in collage layout.");
});
}
const auto aspectHeight = size.height() / float64(size.width());
const auto aspectSkip = st::historyGroupSkip / float64(size.width());
auto wrapped = tag("figure", {
{ "class", "collage" },
{
"style",
("padding-top: " + Percent(aspectHeight) + "%; "
+ "margin-bottom: " + Percent(last ? 0 : aspectSkip) + "%;")
},
}, result);
if (offset + kPerCollage < int(dimensions.size())) {
wrapped += collage(list, dimensions, offset + kPerCollage);
}
return wrapped;
}
QByteArray Parser::block(const MTPDpageBlockUnsupported &data) {
return "Unsupported."_q;
}
@ -289,40 +372,54 @@ QByteArray Parser::block(const MTPDpageBlockPullquote &data) {
rich(data.vtext()) + cite);
}
QByteArray Parser::block(const MTPDpageBlockPhoto &data) {
QByteArray Parser::block(
const MTPDpageBlockPhoto &data,
const Ui::GroupMediaLayout &layout,
QSize outer) {
const auto collage = !layout.geometry.isEmpty();
const auto photo = photoById(data.vphoto_id().v);
if (!photo.id) {
return "Photo not found.";
}
const auto src = photoFullUrl(photo);
auto wrapStyle = QByteArray();
if (photo.width) {
wrapStyle += "max-width:" + QByteArray::number(photo.width) + "px";
if (collage) {
const auto wcoef = 1. / outer.width();
const auto hcoef = 1. / outer.height();
wrapStyle += "left: " + Percent(layout.geometry.x() * wcoef) + "%; "
+ "top: " + Percent(layout.geometry.y() * hcoef) + "%; "
+ "width: " + Percent(layout.geometry.width() * wcoef) + "%; "
+ "height: " + Percent(layout.geometry.height() * hcoef) + "%";
} else if (photo.width) {
wrapStyle += "max-width:" + Number(photo.width) + "px";
}
const auto dimension = collage
? (layout.geometry.height() / float64(layout.geometry.width()))
: (photo.width && photo.height)
? (photo.height / float64(photo.width))
: (3 / 4.);
const auto paddingTop = collage
? Percent(dimension) + "%"
: "calc(min(480px, " + Percent(dimension) + "%))";
const auto style = "background-image:url('" + src + "');"
"padding-top: " + paddingTop + ";";
auto inner = tag("div", {
{ "class", "photo" },
{ "style", style } });
const auto minithumb = Images::ExpandInlineBytes(photo.minithumbnail);
if (!minithumb.isEmpty()) {
const auto image = Images::Read({ .content = minithumb });
wrapStyle += ";background-image:url('data:image/jpeg;base64,"
+ minithumb.toBase64()
+ "');";
inner = tag("div", {
{ "class", "photo-bg" },
{ "style", "background-image:url('data:image/jpeg;base64,"
+ minithumb.toBase64()
+ "');" },
}) + inner;
}
const auto dimension = (photo.width && photo.height)
? (photo.height / float64(photo.width))
: (3 / 4.);
const auto paddingTopPercent = int(base::SafeRound(dimension * 100));
const auto style = "background-image:url('" + src + "');"
"padding-top:" + QByteArray::number(paddingTopPercent) + "%";
const auto inner = tag("div", {
{ "class", "photo" },
{ "style", style } });
auto result = tag(
"div",
{ { "class", "photo-wrap" }, { "style", wrapStyle } },
inner);
if (const auto url = data.vurl()) {
result = tag("a", { { "href", utf(*url) } }, result);
}
auto attributes = Attributes();
auto attributes = Attributes{
{ "class", "photo-wrap" },
{ "style", wrapStyle }
};
if (_captionAsTitle) {
const auto caption = plain(data.vcaption().data().vtext());
const auto credit = plain(data.vcaption().data().vtext());
@ -332,34 +429,89 @@ QByteArray Parser::block(const MTPDpageBlockPhoto &data) {
: (caption + credit);
attributes.push_back({ "title", title });
}
} else {
}
auto result = tag("div", attributes, inner);
const auto href = data.vurl()
? utf(*data.vurl())
: photoFullUrl(photo);
const auto id = Number(photo.id);
result = tag("a", {
{ "href", href },
{ "data-context", data.vurl() ? QByteArray() : "viewer-photo" + id },
}, result);
if (!_captionAsTitle) {
result += caption(data.vcaption());
}
return tag("figure", attributes, result);
return result;
}
QByteArray Parser::block(const MTPDpageBlockVideo &data) {
QByteArray Parser::block(
const MTPDpageBlockVideo &data,
const Ui::GroupMediaLayout &layout,
QSize outer) {
const auto collage = !layout.geometry.isEmpty();
const auto collageSmall = collage
&& (layout.geometry.width() < outer.width());
const auto video = documentById(data.vvideo_id().v);
if (!video.id) {
return "Video not found.";
}
const auto src = documentFullUrl(video);
auto vattributes = Attributes{};
if (collageSmall) {
vattributes.push_back({ "class", "video-small" });
}
if (data.is_autoplay()) {
vattributes.push_back({ "preload", "auto" });
vattributes.push_back({ "autoplay", std::nullopt });
} else {
} else if (!collageSmall) {
vattributes.push_back({ "controls", std::nullopt });
}
if (data.is_loop()) {
vattributes.push_back({ "loop", std::nullopt });
}
vattributes.push_back({ "muted", std::nullopt });
auto result = tag(
auto inner = tag(
"video",
vattributes,
tag("source", { { "src", src }, { "type", "video/mp4" } }));
auto attributes = Attributes();
if (collageSmall) {
inner += tag(
"div",
{ { "class", "video-play-outer" } },
tag("div", { { "class", "video-play" } }));
}
const auto minithumb = Images::ExpandInlineBytes(video.minithumbnail);
if (!minithumb.isEmpty()) {
const auto image = Images::Read({ .content = minithumb });
inner = tag("div", {
{ "class", "video-bg" },
{ "style", "background-image:url('data:image/jpeg;base64,"
+ minithumb.toBase64()
+ "');" },
}) + inner;
}
auto wrapStyle = QByteArray();
if (collage) {
const auto wcoef = 1. / outer.width();
const auto hcoef = 1. / outer.height();
wrapStyle += "left: " + Percent(layout.geometry.x() * wcoef) + "%; "
+ "top: " + Percent(layout.geometry.y() * hcoef) + "%; "
+ "width: " + Percent(layout.geometry.width() * wcoef) + "%; "
+ "height: " + Percent(layout.geometry.height() * hcoef) + "%; ";
} else {
const auto dimension = (video.width && video.height)
? (video.height / float64(video.width))
: (3 / 4.);
wrapStyle += "padding-top: calc(min(480px, "
+ Percent(dimension)
+ "%));";
}
auto attributes = Attributes{
{ "class", "video-wrap" },
{ "style", wrapStyle },
};
if (_captionAsTitle) {
const auto caption = plain(data.vcaption().data().vtext());
const auto credit = plain(data.vcaption().data().vtext());
@ -369,10 +521,20 @@ QByteArray Parser::block(const MTPDpageBlockVideo &data) {
: (caption + credit);
attributes.push_back({ "title", title });
}
} else {
}
auto result = tag("div", attributes, inner);
if (data.is_autoplay() || collageSmall) {
const auto id = Number(video.id);
const auto href = resource("video" + id);
result = tag("a", {
{ "href", href },
{ "data-context", "viewer-video" + id },
}, result);
}
if (!_captionAsTitle) {
result += caption(data.vcaption());
}
return tag("figure", attributes, result);
return result;
}
QByteArray Parser::block(const MTPDpageBlockCover &data) {
@ -394,13 +556,12 @@ QByteArray Parser::block(const MTPDpageBlockEmbed &data) {
eclass = "nowide";
} else if (data.is_full_width() || !data.vw()->v) {
width = "100%";
height = QByteArray::number(data.vh()->v) + "px";
height = Number(data.vh()->v) + "px";
iframeWidth = "100%";
iframeHeight = height;
} else {
const auto percent = data.vh()->v * 100 / data.vw()->v;
width = QByteArray::number(data.vw()->v) + "px";
height = QByteArray::number(percent) + "%";
width = Number(data.vw()->v) + "px";
height = Percent(data.vh()->v / float64(data.vw()->v)) + "%";
}
auto attributes = Attributes();
if (autosize) {
@ -468,11 +629,18 @@ QByteArray Parser::block(const MTPDpageBlockEmbedPost &data) {
}
QByteArray Parser::block(const MTPDpageBlockCollage &data) {
auto result = tag(
const auto &items = data.vitems().v;
const auto dimensions = computeCollageDimensions(items);
if (dimensions.empty()) {
return tag(
"figure",
tag("figure", list(data.vitems())) + caption(data.vcaption()));
}
return tag(
"figure",
{ { "class", "collage" } },
list(data.vitems()));
return tag("figure", result + caption(data.vcaption()));
{ { "class", "collage-wrap" } },
collage(items, dimensions) + caption(data.vcaption()));
}
QByteArray Parser::block(const MTPDpageBlockSlideshow &data) {
@ -482,7 +650,7 @@ QByteArray Parser::block(const MTPDpageBlockSlideshow &data) {
auto attributes = Attributes{
{ "type", "radio" },
{ "name", "s" },
{ "value", QByteArray::number(i) },
{ "value", Number(i) },
{ "onchange", "return IV.slideshowSlide(this);" },
};
if (!i) {
@ -509,7 +677,7 @@ QByteArray Parser::block(const MTPDpageBlockChannel &data) {
auto name = QByteArray();
auto username = QByteArray();
auto id = data.vchannel().match([](const auto &data) {
return QByteArray::number(data.vid().v);
return Number(data.vid().v);
});
data.vchannel().match([&](const MTPDchannel &data) {
if (const auto has = data.vusername()) {
@ -658,7 +826,7 @@ QByteArray Parser::block(const MTPDpageRelatedArticle &data) {
}
const auto webpageId = data.vwebpage_id().v;
const auto context = webpageId
? ("webpage" + QByteArray::number(webpageId))
? ("webpage" + Number(webpageId))
: QByteArray();
return tag("a", {
{ "class", "related-link" },
@ -690,10 +858,10 @@ QByteArray Parser::block(const MTPDpageTableCell &data) {
}
auto attributes = Attributes{ { "style", style } };
if (const auto cs = data.vcolspan()) {
attributes.push_back({ "colspan", QByteArray::number(cs->v) });
attributes.push_back({ "colspan", Number(cs->v) });
}
if (const auto rs = data.vrowspan()) {
attributes.push_back({ "rowspan", QByteArray::number(rs->v) });
attributes.push_back({ "rowspan", Number(rs->v) });
}
return tag(data.is_header() ? "th" : "td", attributes, text);
}
@ -783,10 +951,10 @@ QByteArray Parser::rich(const MTPRichText &text) {
{ "src", documentFullUrl(image) },
};
if (const auto width = data.vw().v) {
attributes.push_back({ "width", QByteArray::number(width) });
attributes.push_back({ "width", Number(width) });
}
if (const auto height = data.vh().v) {
attributes.push_back({ "height", QByteArray::number(height) });
attributes.push_back({ "height", Number(height) });
}
return tag("img", attributes);
}, [&](const MTPDtextBold &data) {
@ -802,7 +970,7 @@ QByteArray Parser::rich(const MTPRichText &text) {
}, [&](const MTPDtextUrl &data) {
const auto webpageId = data.vwebpage_id().v;
const auto context = webpageId
? ("webpage" + QByteArray::number(webpageId))
? ("webpage" + Number(webpageId))
: QByteArray();
return tag("a", {
{ "href", utf(data.vurl()) },
@ -911,6 +1079,24 @@ Document Parser::parse(const MTPDocument &document) {
};
document.match([](const MTPDdocumentEmpty &) {
}, [&](const MTPDdocument &data) {
for (const auto &attribute : data.vattributes().v) {
attribute.match([&](const MTPDdocumentAttributeImageSize &data) {
result.width = data.vw().v;
result.height = data.vh().v;
}, [&](const MTPDdocumentAttributeVideo &data) {
result.width = data.vw().v;
result.height = data.vh().v;
}, [](const auto &) {});
}
if (const auto sizes = data.vthumbs()) {
for (const auto &size : sizes->v) {
size.match([&](const MTPDphotoStrippedSize &data) {
result.minithumbnail = data.vbytes().v;
}, [&](const auto &data) {
});
}
}
});
return result;
}
@ -938,11 +1124,11 @@ Document Parser::documentById(uint64 id) {
}
QByteArray Parser::photoFullUrl(const Photo &photo) {
return resource("photo/" + QByteArray::number(photo.id));
return resource("photo/" + Number(photo.id));
}
QByteArray Parser::documentFullUrl(const Document &document) {
return resource("document/" + QByteArray::number(document.id));
return resource("document/" + Number(document.id));
}
QByteArray Parser::embedUrl(const QByteArray &html) {
@ -969,9 +1155,9 @@ QByteArray Parser::embedUrl(const QByteArray &html) {
QByteArray Parser::mapUrl(const Geo &geo, int width, int height, int zoom) {
return resource("map/"
+ GeoPointId(geo) + "&"
+ QByteArray::number(width) + ","
+ QByteArray::number(height) + "&"
+ QByteArray::number(zoom));
+ Number(width) + ","
+ Number(height) + "&"
+ Number(zoom));
}
QByteArray Parser::resource(QByteArray id) {
@ -982,6 +1168,32 @@ QByteArray Parser::resource(QByteArray id) {
return toFolder ? id : ('/' + id);
}
std::vector<QSize> Parser::computeCollageDimensions(
const QVector<MTPPageBlock> &items) {
auto result = std::vector<QSize>(items.size());
if (items.size() < 2) {
return {};
}
for (auto i = 0, count = int(items.size()); i != count; ++i) {
auto size = QSize();
items[i].match([&](const MTPDpageBlockPhoto &data) {
const auto photo = photoById(data.vphoto_id().v);
if (photo.id && photo.width > 0 && photo.height > 0) {
result[i] = QSize(photo.width, photo.height);
}
}, [&](const MTPDpageBlockVideo &data) {
const auto document = documentById(data.vvideo_id().v);
if (document.id && document.width > 0 && document.height > 0) {
result[i] = QSize(document.width, document.height);
}
}, [](const auto &) {});
if (result[i].isEmpty()) {
return {};
}
}
return result;
}
} // namespace
Prepared Prepare(const Source &source, const Options &options) {