mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-06 11:52:58 +00:00
First attempt to add other media types to IV.
This commit is contained in:
@@ -81,6 +81,11 @@ MarkdownBlockSkips {
|
||||
displayMath: pixels;
|
||||
table: pixels;
|
||||
photo: pixels;
|
||||
video: pixels;
|
||||
audio: pixels;
|
||||
map: pixels;
|
||||
channel: pixels;
|
||||
groupedMedia: pixels;
|
||||
placeholder: pixels;
|
||||
}
|
||||
MarkdownList {
|
||||
@@ -151,6 +156,49 @@ MarkdownPhoto {
|
||||
progressSize: pixels;
|
||||
progressWidth: pixels;
|
||||
}
|
||||
MarkdownAudio {
|
||||
padding: margins;
|
||||
captionSkip: pixels;
|
||||
textSkip: pixels;
|
||||
border: pixels;
|
||||
borderFg: color;
|
||||
bg: color;
|
||||
radius: pixels;
|
||||
titleStyle: TextStyle;
|
||||
titleFg: color;
|
||||
subtitleStyle: TextStyle;
|
||||
subtitleFg: color;
|
||||
}
|
||||
MarkdownChannelButton {
|
||||
padding: margins;
|
||||
border: pixels;
|
||||
borderFg: color;
|
||||
bg: color;
|
||||
radius: pixels;
|
||||
textStyle: TextStyle;
|
||||
textFg: color;
|
||||
}
|
||||
MarkdownChannel {
|
||||
padding: margins;
|
||||
border: pixels;
|
||||
borderFg: color;
|
||||
bg: color;
|
||||
radius: pixels;
|
||||
textSkip: pixels;
|
||||
buttonSkip: pixels;
|
||||
titleStyle: TextStyle;
|
||||
titleFg: color;
|
||||
subtitleStyle: TextStyle;
|
||||
subtitleFg: color;
|
||||
button: MarkdownChannelButton;
|
||||
}
|
||||
MarkdownGroupedMedia {
|
||||
padding: margins;
|
||||
captionSkip: pixels;
|
||||
itemSkip: pixels;
|
||||
radius: pixels;
|
||||
overlayOpacity: double;
|
||||
}
|
||||
MarkdownFailure {
|
||||
label: FlatLabel;
|
||||
width: pixels;
|
||||
@@ -178,6 +226,9 @@ Markdown {
|
||||
details: MarkdownDetails;
|
||||
placeholder: MarkdownPlaceholder;
|
||||
photo: MarkdownPhoto;
|
||||
audio: MarkdownAudio;
|
||||
channel: MarkdownChannel;
|
||||
groupedMedia: MarkdownGroupedMedia;
|
||||
failure: MarkdownFailure;
|
||||
}
|
||||
|
||||
@@ -206,6 +257,11 @@ defaultMarkdownBlockSkips: MarkdownBlockSkips {
|
||||
displayMath: 20px;
|
||||
table: 20px;
|
||||
photo: 20px;
|
||||
video: 20px;
|
||||
audio: 20px;
|
||||
map: 20px;
|
||||
channel: 20px;
|
||||
groupedMedia: 20px;
|
||||
placeholder: 20px;
|
||||
}
|
||||
defaultMarkdownList: MarkdownList {
|
||||
@@ -283,6 +339,60 @@ defaultMarkdownPhoto: MarkdownPhoto {
|
||||
progressSize: 36px;
|
||||
progressWidth: 3px;
|
||||
}
|
||||
defaultMarkdownAudioTitleStyle: TextStyle(defaultMarkdownBodyStyle) {
|
||||
font: font(16px semibold);
|
||||
}
|
||||
defaultMarkdownAudioSubtitleStyle: TextStyle(defaultMarkdownDetailsSummaryStyle) {
|
||||
}
|
||||
defaultMarkdownAudio: MarkdownAudio {
|
||||
padding: margins(8px, 12px, 8px, 12px);
|
||||
captionSkip: 14px;
|
||||
textSkip: 8px;
|
||||
border: 1px;
|
||||
borderFg: inputBorderFg;
|
||||
bg: windowBgOver;
|
||||
radius: 6px;
|
||||
titleStyle: defaultMarkdownAudioTitleStyle;
|
||||
titleFg: windowFg;
|
||||
subtitleStyle: defaultMarkdownAudioSubtitleStyle;
|
||||
subtitleFg: windowSubTextFg;
|
||||
}
|
||||
defaultMarkdownChannelTitleStyle: TextStyle(defaultMarkdownAudioTitleStyle) {
|
||||
}
|
||||
defaultMarkdownChannelSubtitleStyle: TextStyle(defaultMarkdownDetailsSummaryStyle) {
|
||||
}
|
||||
defaultMarkdownChannelButtonStyle: TextStyle(defaultMarkdownDetailsSummaryStyle) {
|
||||
}
|
||||
defaultMarkdownChannelButton: MarkdownChannelButton {
|
||||
padding: margins(10px, 12px, 10px, 12px);
|
||||
border: 1px;
|
||||
borderFg: windowActiveTextFg;
|
||||
bg: windowBg;
|
||||
radius: 6px;
|
||||
textStyle: defaultMarkdownChannelButtonStyle;
|
||||
textFg: windowActiveTextFg;
|
||||
}
|
||||
defaultMarkdownChannel: MarkdownChannel {
|
||||
padding: margins(8px, 12px, 8px, 12px);
|
||||
border: 1px;
|
||||
borderFg: inputBorderFg;
|
||||
bg: windowBgOver;
|
||||
radius: 6px;
|
||||
textSkip: 8px;
|
||||
buttonSkip: 8px;
|
||||
titleStyle: defaultMarkdownChannelTitleStyle;
|
||||
titleFg: windowFg;
|
||||
subtitleStyle: defaultMarkdownChannelSubtitleStyle;
|
||||
subtitleFg: windowSubTextFg;
|
||||
button: defaultMarkdownChannelButton;
|
||||
}
|
||||
defaultMarkdownGroupedMedia: MarkdownGroupedMedia {
|
||||
padding: margins(4px, 0px, 0px, 0px);
|
||||
captionSkip: 12px;
|
||||
itemSkip: 4px;
|
||||
radius: 6px;
|
||||
overlayOpacity: 0.2;
|
||||
}
|
||||
defaultMarkdownFailureLabel: FlatLabel(defaultFlatLabel) {
|
||||
minWidth: 280px;
|
||||
textFg: windowSubTextFg;
|
||||
@@ -346,5 +456,8 @@ defaultMarkdown: Markdown {
|
||||
details: defaultMarkdownDetails;
|
||||
placeholder: defaultMarkdownPlaceholder;
|
||||
photo: defaultMarkdownPhoto;
|
||||
audio: defaultMarkdownAudio;
|
||||
channel: defaultMarkdownChannel;
|
||||
groupedMedia: defaultMarkdownGroupedMedia;
|
||||
failure: defaultMarkdownFailure;
|
||||
}
|
||||
|
||||
@@ -50,12 +50,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/basic_click_handlers.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "ui/painter.h"
|
||||
#include "webview/webview_data_stream_memory.h"
|
||||
#include "webview/webview_interface.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/window_session_controller_link_info.h"
|
||||
|
||||
#include "styles/palette.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtGui/QGuiApplication>
|
||||
@@ -64,6 +68,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <optional>
|
||||
|
||||
namespace Iv {
|
||||
|
||||
struct NativeIvChannelContext {
|
||||
uint64 channelId = 0;
|
||||
QString username;
|
||||
};
|
||||
|
||||
[[nodiscard]] NativeIvChannelContext ParseNativeIvChannelContext(
|
||||
const QString &context) {
|
||||
const auto separator = context.indexOf(u'\n');
|
||||
return {
|
||||
.channelId = (separator >= 0)
|
||||
? context.mid(0, separator).toULongLong()
|
||||
: context.toULongLong(),
|
||||
.username = (separator >= 0) ? context.mid(separator + 1) : QString(),
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] QString SerializeNativeIvChannelContext(
|
||||
uint64 channelId,
|
||||
QString username) {
|
||||
auto result = QString::number(channelId);
|
||||
if (!username.isEmpty()) {
|
||||
result += u"\n"_q + username;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString ResolveNativeIvChannelUsername(
|
||||
const QString &channelUsername,
|
||||
const QString &contextUsername) {
|
||||
return !channelUsername.isEmpty() ? channelUsername : contextUsername;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kGeoPointScale = 1;
|
||||
@@ -242,13 +279,297 @@ private:
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] ImageWithLocation CachedPageMapImageData(
|
||||
double latitude,
|
||||
double longitude,
|
||||
uint64 accessHash,
|
||||
QSize size,
|
||||
int zoom) {
|
||||
const auto location = GeoPointLocation{
|
||||
.lat = latitude,
|
||||
.lon = longitude,
|
||||
.access = accessHash,
|
||||
.width = std::max(size.width(), 1),
|
||||
.height = std::max(size.height(), 1),
|
||||
.zoom = std::max(zoom, kGeoPointZoomMin),
|
||||
.scale = kGeoPointScale,
|
||||
};
|
||||
return {
|
||||
.location = ImageLocation(
|
||||
{ location },
|
||||
location.width,
|
||||
location.height),
|
||||
};
|
||||
}
|
||||
|
||||
class CachedPageDocumentRuntime final : public Markdown::DocumentRuntime {
|
||||
public:
|
||||
CachedPageDocumentRuntime(
|
||||
not_null<Main::Session*> session,
|
||||
not_null<DocumentData*> document,
|
||||
::Data::FileOrigin origin)
|
||||
: _session(session)
|
||||
, _document(document)
|
||||
, _origin(std::move(origin))
|
||||
, _media(document->createMediaView()) {
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> thumbnail(
|
||||
QSize size) const override {
|
||||
Q_UNUSED(size);
|
||||
return Ui::MakeDocumentThumbnailFit(_document, _origin);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> full(
|
||||
QSize size) const override {
|
||||
Q_UNUSED(size);
|
||||
return Ui::MakeDocumentThumbnail(_document, _origin);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool loaded() const override {
|
||||
return _media->loaded();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool loading() const override {
|
||||
return _document->displayLoading();
|
||||
}
|
||||
|
||||
[[nodiscard]] double progress() const override {
|
||||
return _document->progress();
|
||||
}
|
||||
|
||||
void open(Qt::MouseButton button) const override {
|
||||
if (button != Qt::LeftButton && button != Qt::MiddleButton) {
|
||||
return;
|
||||
}
|
||||
if (const auto window = Core::App().activeWindow()) {
|
||||
const auto item = (HistoryItem*)nullptr;
|
||||
window->openInMediaView({
|
||||
CurrentSessionController(_session),
|
||||
_document,
|
||||
item,
|
||||
MsgId(0),
|
||||
PeerId(0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const not_null<Main::Session*> _session;
|
||||
const not_null<DocumentData*> _document;
|
||||
const ::Data::FileOrigin _origin;
|
||||
const std::shared_ptr<::Data::DocumentMedia> _media;
|
||||
|
||||
};
|
||||
|
||||
class CachedPageMapDynamicImage final : public Ui::DynamicImage {
|
||||
public:
|
||||
CachedPageMapDynamicImage(
|
||||
not_null<::Data::CloudImage*> data,
|
||||
not_null<Main::Session*> session,
|
||||
::Data::FileOrigin origin)
|
||||
: _data(data)
|
||||
, _session(session)
|
||||
, _origin(std::move(origin)) {
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> clone() override {
|
||||
return std::make_shared<CachedPageMapDynamicImage>(
|
||||
_data,
|
||||
_session,
|
||||
_origin);
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage image(int size) override {
|
||||
Q_UNUSED(size);
|
||||
const auto loaded = _view ? *_view : QImage();
|
||||
if (loaded.isNull()) {
|
||||
return QImage();
|
||||
}
|
||||
const auto paletteVersion = style::PaletteVersion();
|
||||
if (_prepared.size() == loaded.size()
|
||||
&& _prepared.devicePixelRatio() == loaded.devicePixelRatio()
|
||||
&& _paletteVersion == paletteVersion) {
|
||||
return _prepared;
|
||||
}
|
||||
_paletteVersion = paletteVersion;
|
||||
_prepared = loaded.copy();
|
||||
_prepared.setDevicePixelRatio(loaded.devicePixelRatio());
|
||||
const auto ratio = loaded.devicePixelRatio();
|
||||
const auto width = int(loaded.width() / ratio);
|
||||
const auto height = int(loaded.height() / ratio);
|
||||
const auto markerSize = std::min(width, height);
|
||||
auto p = Painter(&_prepared);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto pinScale = std::min({
|
||||
1.0,
|
||||
width / (st::historyMapPoint.height() * 2.5),
|
||||
height / (st::historyMapPoint.height() * 2.5),
|
||||
});
|
||||
const auto center = QPointF(width / 2.0, height / 2.0);
|
||||
p.translate(center);
|
||||
p.scale(pinScale, pinScale);
|
||||
p.translate(-center);
|
||||
const auto paintMarker = [&](const style::icon &icon) {
|
||||
icon.paint(
|
||||
p,
|
||||
(width - icon.width()) / 2,
|
||||
(height / 2) - icon.height(),
|
||||
markerSize);
|
||||
};
|
||||
paintMarker(st::historyMapPoint);
|
||||
paintMarker(st::historyMapPointInner);
|
||||
return _prepared;
|
||||
}
|
||||
|
||||
void subscribeToUpdates(Fn<void()> callback) override {
|
||||
_subscription.destroy();
|
||||
if (!callback) {
|
||||
_view = nullptr;
|
||||
_prepared = QImage();
|
||||
return;
|
||||
}
|
||||
_view = _data->createView();
|
||||
_data->load(_session, _origin);
|
||||
if (!_view->isNull()) {
|
||||
return;
|
||||
}
|
||||
_subscription = _session->downloaderTaskFinished(
|
||||
) | rpl::filter([=] {
|
||||
return !_view->isNull();
|
||||
}) | rpl::take(1) | rpl::on_next([=] {
|
||||
_prepared = QImage();
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
const not_null<::Data::CloudImage*> _data;
|
||||
const not_null<Main::Session*> _session;
|
||||
const ::Data::FileOrigin _origin;
|
||||
std::shared_ptr<QImage> _view;
|
||||
QImage _prepared;
|
||||
int _paletteVersion = 0;
|
||||
rpl::lifetime _subscription;
|
||||
|
||||
};
|
||||
|
||||
class CachedPageMapRuntime final : public Markdown::MapRuntime {
|
||||
public:
|
||||
CachedPageMapRuntime(
|
||||
not_null<Main::Session*> session,
|
||||
::Data::FileOrigin origin,
|
||||
double latitude,
|
||||
double longitude,
|
||||
uint64 accessHash,
|
||||
QSize size,
|
||||
int zoom)
|
||||
: _session(session)
|
||||
, _origin(std::move(origin))
|
||||
, _image(session, CachedPageMapImageData(
|
||||
latitude,
|
||||
longitude,
|
||||
accessHash,
|
||||
size,
|
||||
zoom)) {
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> thumbnail(
|
||||
QSize size) const override {
|
||||
Q_UNUSED(size);
|
||||
ensureLoaded();
|
||||
return std::make_shared<CachedPageMapDynamicImage>(
|
||||
&_image,
|
||||
_session,
|
||||
_origin);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> full(
|
||||
QSize size) const override {
|
||||
Q_UNUSED(size);
|
||||
ensureLoaded();
|
||||
return std::make_shared<CachedPageMapDynamicImage>(
|
||||
&_image,
|
||||
_session,
|
||||
_origin);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool loaded() const override {
|
||||
ensureLoaded();
|
||||
return _image.loadedOnce();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool loading() const override {
|
||||
ensureLoaded();
|
||||
return _image.loading();
|
||||
}
|
||||
|
||||
[[nodiscard]] double progress() const override {
|
||||
ensureLoaded();
|
||||
return _image.loadedOnce() ? 1. : 0.;
|
||||
}
|
||||
|
||||
private:
|
||||
void ensureLoaded() const {
|
||||
_image.load(_session, _origin);
|
||||
}
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
const ::Data::FileOrigin _origin;
|
||||
mutable ::Data::CloudImage _image;
|
||||
|
||||
};
|
||||
|
||||
class CachedPageChannelRuntime final : public Markdown::ChannelRuntime {
|
||||
public:
|
||||
CachedPageChannelRuntime(
|
||||
not_null<ChannelData*> channel,
|
||||
QString context,
|
||||
Fn<void(QString)> openChannel,
|
||||
Fn<void(QString)> joinChannel)
|
||||
: _channel(channel)
|
||||
, _context(std::move(context))
|
||||
, _openChannel(std::move(openChannel))
|
||||
, _joinChannel(std::move(joinChannel)) {
|
||||
}
|
||||
|
||||
[[nodiscard]] bool joinVisible() const override {
|
||||
return !_channel->amIn();
|
||||
}
|
||||
|
||||
void open(Qt::MouseButton button) const override {
|
||||
if ((button == Qt::LeftButton || button == Qt::MiddleButton)
|
||||
&& _openChannel) {
|
||||
_openChannel(_context);
|
||||
}
|
||||
}
|
||||
|
||||
void join(Qt::MouseButton button) const override {
|
||||
if ((button == Qt::LeftButton || button == Qt::MiddleButton)
|
||||
&& _joinChannel) {
|
||||
_joinChannel(_context);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const not_null<ChannelData*> _channel;
|
||||
const QString _context;
|
||||
const Fn<void(QString)> _openChannel;
|
||||
const Fn<void(QString)> _joinChannel;
|
||||
|
||||
};
|
||||
|
||||
class CachedPageMediaRuntime final : public Markdown::MediaRuntime {
|
||||
public:
|
||||
CachedPageMediaRuntime(
|
||||
not_null<Main::Session*> session,
|
||||
not_null<WebPageData*> page)
|
||||
not_null<WebPageData*> page,
|
||||
Fn<void(QString)> openChannel,
|
||||
Fn<void(QString)> joinChannel)
|
||||
: _session(session)
|
||||
, _page(page) {
|
||||
, _page(page)
|
||||
, _openChannel(std::move(openChannel))
|
||||
, _joinChannel(std::move(joinChannel)) {
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> resolveInlineImage(
|
||||
@@ -274,13 +595,73 @@ public:
|
||||
fileOrigin());
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Markdown::DocumentRuntime> resolveDocument(
|
||||
uint64 documentId) const override {
|
||||
const auto document = _session->data().document(DocumentId(documentId));
|
||||
if (document->isNull()) {
|
||||
return nullptr;
|
||||
}
|
||||
return std::make_shared<CachedPageDocumentRuntime>(
|
||||
_session,
|
||||
document,
|
||||
fileOrigin());
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Markdown::MapRuntime> resolveMap(
|
||||
double latitude,
|
||||
double longitude,
|
||||
uint64 accessHash,
|
||||
QSize size,
|
||||
int zoom) const override {
|
||||
return std::make_shared<CachedPageMapRuntime>(
|
||||
_session,
|
||||
fileOrigin(),
|
||||
latitude,
|
||||
longitude,
|
||||
accessHash,
|
||||
size,
|
||||
zoom);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Markdown::ChannelRuntime> resolveChannel(
|
||||
uint64 channelId,
|
||||
const QString &username) const override {
|
||||
const auto channel = _session->data().channel(ChannelId(channelId));
|
||||
subscribeToChannel(channelId, channel);
|
||||
return std::make_shared<CachedPageChannelRuntime>(
|
||||
channel,
|
||||
SerializeNativeIvChannelContext(channelId, username),
|
||||
_openChannel,
|
||||
_joinChannel);
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<uint64> channelJoinedChanges() const override {
|
||||
return _channelJoinedChanges.events();
|
||||
}
|
||||
|
||||
private:
|
||||
void subscribeToChannel(
|
||||
uint64 channelId,
|
||||
not_null<ChannelData*> channel) const {
|
||||
if (_channelJoinedSubscriptions.find(channelId)
|
||||
!= end(_channelJoinedSubscriptions)) {
|
||||
return;
|
||||
}
|
||||
Info::Profile::AmInChannelValue(channel) | rpl::on_next([=](bool) {
|
||||
_channelJoinedChanges.fire_copy(channelId);
|
||||
}, _channelJoinedSubscriptions[channelId]);
|
||||
}
|
||||
|
||||
[[nodiscard]] ::Data::FileOrigin fileOrigin() const {
|
||||
return ::Data::FileOriginWebPage{ _page->url };
|
||||
}
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
const not_null<WebPageData*> _page;
|
||||
const Fn<void(QString)> _openChannel;
|
||||
const Fn<void(QString)> _joinChannel;
|
||||
mutable base::flat_map<uint64, rpl::lifetime> _channelJoinedSubscriptions;
|
||||
mutable rpl::event_stream<uint64> _channelJoinedChanges;
|
||||
|
||||
};
|
||||
|
||||
@@ -405,7 +786,9 @@ public:
|
||||
not_null<Delegate*> delegate,
|
||||
not_null<Main::Session*> session,
|
||||
not_null<Data*> data,
|
||||
QString hash);
|
||||
QString hash,
|
||||
Fn<void(QString)> openChannel,
|
||||
Fn<void(QString)> joinChannel);
|
||||
|
||||
[[nodiscard]] bool showing(
|
||||
not_null<Main::Session*> session,
|
||||
@@ -506,6 +889,8 @@ private:
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
const not_null<Main::Session*> _session;
|
||||
const Fn<void(QString)> _openChannel;
|
||||
const Fn<void(QString)> _joinChannel;
|
||||
std::shared_ptr<Main::SessionShow> _show;
|
||||
QString _id;
|
||||
std::unique_ptr<Controller> _controller;
|
||||
@@ -568,9 +953,13 @@ Shown::Shown(
|
||||
not_null<Delegate*> delegate,
|
||||
not_null<Main::Session*> session,
|
||||
not_null<Data*> data,
|
||||
QString hash)
|
||||
QString hash,
|
||||
Fn<void(QString)> openChannel,
|
||||
Fn<void(QString)> joinChannel)
|
||||
: _delegate(delegate)
|
||||
, _session(session) {
|
||||
, _session(session)
|
||||
, _openChannel(std::move(openChannel))
|
||||
, _joinChannel(std::move(joinChannel)) {
|
||||
prepare(data, hash);
|
||||
}
|
||||
|
||||
@@ -877,7 +1266,11 @@ void Shown::showMarkdownWindowed(
|
||||
|
||||
std::shared_ptr<Markdown::MediaRuntime> Shown::createMediaRuntime(
|
||||
not_null<WebPageData*> page) const {
|
||||
return std::make_shared<CachedPageMediaRuntime>(_session, page);
|
||||
return std::make_shared<CachedPageMediaRuntime>(
|
||||
_session,
|
||||
page,
|
||||
_openChannel,
|
||||
_joinChannel);
|
||||
}
|
||||
|
||||
bool Shown::activateMarkdownMedia(
|
||||
@@ -902,6 +1295,24 @@ bool Shown::activateMarkdownMedia(
|
||||
}
|
||||
activation.photo->open(button);
|
||||
return true;
|
||||
case Markdown::MediaActivationKind::Document:
|
||||
if (!activation.document) {
|
||||
return false;
|
||||
}
|
||||
activation.document->open(button);
|
||||
return true;
|
||||
case Markdown::MediaActivationKind::OpenChannel:
|
||||
if (!activation.channel) {
|
||||
return false;
|
||||
}
|
||||
activation.channel->open(button);
|
||||
return true;
|
||||
case Markdown::MediaActivationKind::JoinChannel:
|
||||
if (!activation.channel) {
|
||||
return false;
|
||||
}
|
||||
activation.channel->join(button);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1261,6 +1672,8 @@ void Shown::update(not_null<Data*> data) {
|
||||
void Shown::showJoinedTooltip() {
|
||||
if (_controller) {
|
||||
_controller->showJoinedTooltip();
|
||||
} else if (_markdownController) {
|
||||
_markdownController->showJoinedTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1353,7 +1766,17 @@ void Instance::show(
|
||||
_shown->moveTo(data, hash);
|
||||
return;
|
||||
}
|
||||
_shown = std::make_unique<Shown>(_delegate, session, data, hash);
|
||||
_shown = std::make_unique<Shown>(
|
||||
_delegate,
|
||||
session,
|
||||
data,
|
||||
hash,
|
||||
[=](QString context) {
|
||||
processOpenChannel(context);
|
||||
},
|
||||
[=](QString context) {
|
||||
processJoinChannel(context);
|
||||
});
|
||||
_shownSession = session;
|
||||
_shown->events() | rpl::on_next([=](Controller::Event event) {
|
||||
using Type = Controller::Event::Type;
|
||||
@@ -1730,17 +2153,21 @@ WebPageData *Instance::processReceivedPage(
|
||||
void Instance::processOpenChannel(const QString &context) {
|
||||
if (!_shownSession) {
|
||||
return;
|
||||
} else if (const auto channelId = ChannelId(context.toLongLong())) {
|
||||
}
|
||||
const auto parsed = ParseNativeIvChannelContext(context);
|
||||
if (const auto channelId = ChannelId(parsed.channelId)) {
|
||||
const auto channel = _shownSession->data().channel(channelId);
|
||||
if (channel->isLoaded()) {
|
||||
if (const auto controller = _shownSession->tryResolveWindow(channel)) {
|
||||
controller->showPeerHistory(channel);
|
||||
_shown = nullptr;
|
||||
}
|
||||
} else if (!channel->username().isEmpty()) {
|
||||
} else if (const auto username = ResolveNativeIvChannelUsername(
|
||||
channel->username(),
|
||||
parsed.username); !username.isEmpty()) {
|
||||
if (const auto controller = _shownSession->tryResolveWindow(channel)) {
|
||||
controller->showPeerByLink({
|
||||
.usernameOrId = channel->username(),
|
||||
.usernameOrId = username,
|
||||
});
|
||||
_shown = nullptr;
|
||||
}
|
||||
@@ -1751,15 +2178,19 @@ void Instance::processOpenChannel(const QString &context) {
|
||||
void Instance::processJoinChannel(const QString &context) {
|
||||
if (!_shownSession) {
|
||||
return;
|
||||
} else if (const auto channelId = ChannelId(context.toLongLong())) {
|
||||
}
|
||||
const auto parsed = ParseNativeIvChannelContext(context);
|
||||
if (const auto channelId = ChannelId(parsed.channelId)) {
|
||||
const auto channel = _shownSession->data().channel(channelId);
|
||||
_joining[_shownSession].emplace(channel);
|
||||
if (channel->isLoaded()) {
|
||||
_shownSession->api().joinChannel(channel);
|
||||
} else if (!channel->username().isEmpty()) {
|
||||
} else if (const auto username = ResolveNativeIvChannelUsername(
|
||||
channel->username(),
|
||||
parsed.username); !username.isEmpty()) {
|
||||
if (const auto controller = _shownSession->tryResolveWindow(channel)) {
|
||||
controller->showPeerByLink({
|
||||
.usernameOrId = channel->username(),
|
||||
.usernameOrId = username,
|
||||
.joinChannel = true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,13 +229,35 @@ void RebuildVisibleSegmentLookup(
|
||||
auto result = HitSegmentBoundary(
|
||||
segment,
|
||||
after ? SegmentLength(segment) : 0);
|
||||
if (segment.block) {
|
||||
result.mediaActivation = segment.block->activation;
|
||||
if (const auto prepared = PreparedLinkForMediaActivation(
|
||||
result.mediaActivation)) {
|
||||
const auto applyActivation = [&](const MediaActivation &activation) {
|
||||
result.mediaActivation = activation;
|
||||
if (const auto prepared = PreparedLinkForMediaActivation(activation)) {
|
||||
result.preparedLink = prepared;
|
||||
result.state.link = CreatePreparedLinkHandler(*prepared);
|
||||
}
|
||||
};
|
||||
if (segment.block) {
|
||||
if (!segment.block->actionRect.isEmpty()
|
||||
&& segment.block->actionRect.contains(point)
|
||||
&& segment.block->channelRuntime
|
||||
&& segment.block->channelRuntime->joinVisible()) {
|
||||
applyActivation(segment.block->actionActivation);
|
||||
} else if (segment.block->kind == PreparedBlockKind::GroupedMedia) {
|
||||
auto matchedItem = false;
|
||||
for (const auto &item : segment.block->groupedMediaItems) {
|
||||
if (!item.rect.contains(point)) {
|
||||
continue;
|
||||
}
|
||||
applyActivation(item.activation);
|
||||
matchedItem = true;
|
||||
break;
|
||||
}
|
||||
if (!matchedItem) {
|
||||
applyActivation(segment.block->activation);
|
||||
}
|
||||
} else {
|
||||
applyActivation(segment.block->activation);
|
||||
}
|
||||
}
|
||||
result.direct = true;
|
||||
return result;
|
||||
@@ -548,6 +570,18 @@ public:
|
||||
ClearColorizedFormulaImages(&_blocks);
|
||||
}
|
||||
|
||||
void invalidateLayout() {
|
||||
_width = -1;
|
||||
_height = 0;
|
||||
clearPendingHighlightBlockPointers();
|
||||
_blocks.clear();
|
||||
_anchors.clear();
|
||||
_segments.clear();
|
||||
_visibleSegmentSpan = {};
|
||||
_segmentTops.clear();
|
||||
_segmentBottoms.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] int currentDevicePixelRatio() const {
|
||||
return std::max(style::DevicePixelRatio(), 1);
|
||||
@@ -634,18 +668,6 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void invalidateLayout() {
|
||||
_width = -1;
|
||||
_height = 0;
|
||||
clearPendingHighlightBlockPointers();
|
||||
_blocks.clear();
|
||||
_anchors.clear();
|
||||
_segments.clear();
|
||||
_visibleSegmentSpan = {};
|
||||
_segmentTops.clear();
|
||||
_segmentBottoms.clear();
|
||||
}
|
||||
|
||||
void resetFormulaRasterCache() {
|
||||
_formulaRenders.clear();
|
||||
_formulaRenders.resize(_content.formulas.size());
|
||||
@@ -727,6 +749,10 @@ void MarkdownArticle::setContent(MarkdownArticleContent content) {
|
||||
_impl->setContent(std::move(content));
|
||||
}
|
||||
|
||||
void MarkdownArticle::invalidateLayout() {
|
||||
_impl->invalidateLayout();
|
||||
}
|
||||
|
||||
int MarkdownArticle::maxWidth() const {
|
||||
return const_cast<Impl*>(_impl.get())->maxWidth();
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ public:
|
||||
|
||||
void setRenderer(std::shared_ptr<MathRenderer> renderer);
|
||||
void setContent(MarkdownArticleContent content);
|
||||
void invalidateLayout();
|
||||
[[nodiscard]] int maxWidth() const;
|
||||
[[nodiscard]] int resizeGetHeight(int width);
|
||||
void setVisibleTopBottom(int visibleTop, int visibleBottom);
|
||||
|
||||
@@ -9,11 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "iv/markdown/iv_markdown_article_text.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "spellcheck/spellcheck_highlight_syntax.h"
|
||||
#include "ui/grouped_layout.h"
|
||||
|
||||
#include "styles/style_iv.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
namespace Iv::Markdown {
|
||||
@@ -29,6 +31,7 @@ constexpr auto kIvMarkedTextOptions = TextParseOptions{
|
||||
constexpr auto kCodeTabColumns = 4;
|
||||
constexpr auto kCodeTrailingGuard = 0x2060;
|
||||
const auto kPhotoCopyLabel = u"Photo"_q;
|
||||
const auto kUsernamePrefix = u"@"_q;
|
||||
|
||||
[[nodiscard]] style::align CellAlign(TableAlignment alignment) {
|
||||
switch (alignment) {
|
||||
@@ -148,6 +151,219 @@ const auto kPhotoCopyLabel = u"Photo"_q;
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Runtime>
|
||||
void ResolveRuntimeImages(
|
||||
const std::shared_ptr<Runtime> &runtime,
|
||||
QSize size,
|
||||
std::shared_ptr<Ui::DynamicImage> *thumbnail,
|
||||
std::shared_ptr<Ui::DynamicImage> *full) {
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
if (thumbnail) {
|
||||
*thumbnail = runtime->thumbnail(size);
|
||||
}
|
||||
if (full) {
|
||||
*full = runtime->full(size);
|
||||
}
|
||||
}
|
||||
|
||||
void SetPlainTextLeaf(
|
||||
Ui::Text::String *leaf,
|
||||
const style::TextStyle &textStyle,
|
||||
const QString &text,
|
||||
int width) {
|
||||
*leaf = Ui::Text::String(TextMinResizeWidth(width));
|
||||
leaf->setMarkedText(
|
||||
textStyle,
|
||||
TextWithEntities::Simple(text),
|
||||
kIvMarkedTextOptions);
|
||||
}
|
||||
|
||||
[[nodiscard]] int LeafHeight(
|
||||
const Ui::Text::String &leaf,
|
||||
const style::TextStyle &textStyle,
|
||||
int width) {
|
||||
return std::max(
|
||||
leaf.countHeight(width, true),
|
||||
TextLineHeight(textStyle));
|
||||
}
|
||||
|
||||
[[nodiscard]] int MediaHeightForWidth(
|
||||
int width,
|
||||
int aspectWidth,
|
||||
int aspectHeight) {
|
||||
aspectWidth = std::max(aspectWidth, 1);
|
||||
aspectHeight = std::max(aspectHeight, 1);
|
||||
return std::max(
|
||||
int((int64(width) * aspectHeight + aspectWidth - 1) / aspectWidth),
|
||||
1);
|
||||
}
|
||||
|
||||
[[nodiscard]] int GroupedMediaMinWidth(int width, int spacing) {
|
||||
return std::max((width - 2 * spacing) / 3, 1);
|
||||
}
|
||||
|
||||
void LayoutMediaCaption(
|
||||
LaidOutBlock *block,
|
||||
const PreparedBlock &prepared,
|
||||
const std::vector<PreparedFormulaSlot> *formulas,
|
||||
InlineFormulaObjectCache *inlineFormulaObjects,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width,
|
||||
int skip,
|
||||
int *bottom) {
|
||||
if (prepared.text.text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
block->textWidth = std::max(width, 1);
|
||||
SetTextLeaf(
|
||||
&block->leaf,
|
||||
markdown.body,
|
||||
prepared.text,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
block->textWidth);
|
||||
BindLinks(&block->leaf, prepared.links);
|
||||
const auto captionTop = top + skip;
|
||||
const auto captionHeight = std::max(
|
||||
block->leaf.countHeight(block->textWidth, true),
|
||||
TextLineHeight(markdown.body));
|
||||
block->textRect = QRect(left, captionTop, block->textWidth, captionHeight);
|
||||
*bottom = captionTop + captionHeight;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString AudioTitleText(const PreparedAudioBlockData &audio) {
|
||||
if (!audio.title.isEmpty()) {
|
||||
return audio.title;
|
||||
}
|
||||
if (!audio.fileName.isEmpty()) {
|
||||
return audio.fileName;
|
||||
}
|
||||
return tr::lng_in_dlg_audio_file(tr::now);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString AudioSubtitleText(const PreparedAudioBlockData &audio) {
|
||||
if (!audio.performer.isEmpty()) {
|
||||
return audio.performer;
|
||||
}
|
||||
if (!audio.fileName.isEmpty() && audio.fileName != AudioTitleText(audio)) {
|
||||
return audio.fileName;
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
[[nodiscard]] QString AudioCopyText(const PreparedAudioBlockData &audio) {
|
||||
const auto title = AudioTitleText(audio);
|
||||
const auto subtitle = AudioSubtitleText(audio);
|
||||
return subtitle.isEmpty() ? title : (title + u"\n"_q + subtitle);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString ChannelSubtitleText(
|
||||
const PreparedChannelBlockData &channel) {
|
||||
return channel.username.isEmpty()
|
||||
? QString()
|
||||
: (kUsernamePrefix + channel.username);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString ChannelCopyText(const PreparedChannelBlockData &channel) {
|
||||
const auto subtitle = ChannelSubtitleText(channel);
|
||||
return subtitle.isEmpty()
|
||||
? channel.title
|
||||
: (channel.title + u"\n"_q + subtitle);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString GroupedMediaCopyText(
|
||||
const PreparedGroupedMediaBlockData &grouped) {
|
||||
auto photos = 0;
|
||||
auto videos = 0;
|
||||
for (const auto &item : grouped.items) {
|
||||
if (item.media.kind == PreparedMediaItemKind::Photo) {
|
||||
++photos;
|
||||
} else {
|
||||
++videos;
|
||||
}
|
||||
}
|
||||
if (photos && !videos) {
|
||||
return tr::lng_media_selected_photo(tr::now, lt_count, photos);
|
||||
} else if (videos && !photos) {
|
||||
return tr::lng_media_selected_video(tr::now, lt_count, videos);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
[[nodiscard]] QString GroupedMediaItemCopyText(PreparedMediaItemKind kind) {
|
||||
return (kind == PreparedMediaItemKind::Photo)
|
||||
? kPhotoCopyLabel
|
||||
: tr::lng_in_dlg_video(tr::now);
|
||||
}
|
||||
|
||||
[[nodiscard]] int GroupedMediaLayoutWidth(
|
||||
const std::vector<Ui::GroupMediaLayout> &layout) {
|
||||
auto result = 0;
|
||||
for (const auto &part : layout) {
|
||||
result = std::max(
|
||||
result,
|
||||
part.geometry.x() + part.geometry.width());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] int GroupedMediaLayoutHeight(
|
||||
const std::vector<Ui::GroupMediaLayout> &layout) {
|
||||
auto result = 0;
|
||||
for (const auto &part : layout) {
|
||||
result = std::max(
|
||||
result,
|
||||
part.geometry.y() + part.geometry.height());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void ResolveGroupedMediaItemLayout(
|
||||
LaidOutGroupedMediaItem *item,
|
||||
const PreparedGroupedMediaItemData &prepared,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
QRect rect) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
item->kind = prepared.media.kind;
|
||||
item->copyText = GroupedMediaItemCopyText(prepared.media.kind);
|
||||
item->rect = rect;
|
||||
if (prepared.media.kind == PreparedMediaItemKind::Photo) {
|
||||
if (mediaRuntime) {
|
||||
item->photoRuntime = mediaRuntime->resolvePhoto(prepared.media.id);
|
||||
}
|
||||
ResolveRuntimeImages(
|
||||
item->photoRuntime,
|
||||
rect.size(),
|
||||
&item->thumbnailImage,
|
||||
&item->fullImage);
|
||||
if (item->photoRuntime) {
|
||||
item->activation.kind = MediaActivationKind::Photo;
|
||||
item->activation.photo = item->photoRuntime;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mediaRuntime) {
|
||||
item->documentRuntime = mediaRuntime->resolveDocument(prepared.media.id);
|
||||
}
|
||||
ResolveRuntimeImages(
|
||||
item->documentRuntime,
|
||||
rect.size(),
|
||||
&item->thumbnailImage,
|
||||
&item->fullImage);
|
||||
if (item->documentRuntime) {
|
||||
item->activation.kind = MediaActivationKind::Document;
|
||||
item->activation.document = item->documentRuntime;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
[[nodiscard]] int SingleDigitOrderedMarkerWidth(
|
||||
@@ -286,10 +502,20 @@ int BlockSkip(
|
||||
return skips.table;
|
||||
case PreparedBlockKind::Photo:
|
||||
return skips.photo;
|
||||
case PreparedBlockKind::Video:
|
||||
return skips.video;
|
||||
case PreparedBlockKind::Audio:
|
||||
return skips.audio;
|
||||
case PreparedBlockKind::Map:
|
||||
return skips.map;
|
||||
case PreparedBlockKind::Channel:
|
||||
return skips.channel;
|
||||
case PreparedBlockKind::Placeholder:
|
||||
return skips.placeholder;
|
||||
case PreparedBlockKind::Details:
|
||||
return skips.paragraph;
|
||||
case PreparedBlockKind::GroupedMedia:
|
||||
return skips.groupedMedia;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -371,6 +597,17 @@ LaidOutBlock LayoutFlowBlock(
|
||||
int left,
|
||||
int top,
|
||||
int width) {
|
||||
if (prepared.kind == PreparedBlockKind::GroupedMedia) {
|
||||
return LayoutGroupedMediaBlock(
|
||||
prepared,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
markdown,
|
||||
left,
|
||||
top,
|
||||
width);
|
||||
}
|
||||
auto block = LaidOutBlock();
|
||||
block.kind = prepared.kind;
|
||||
block.anchorId = prepared.anchorId;
|
||||
@@ -717,28 +954,18 @@ LaidOutBlock LayoutPlaceholderBlock(
|
||||
style.labelStyle);
|
||||
|
||||
auto bottom = top + mediaHeight;
|
||||
if (!prepared.text.text.isEmpty()) {
|
||||
block.textWidth = contentWidth;
|
||||
SetTextLeaf(
|
||||
&block.leaf,
|
||||
markdown.body,
|
||||
prepared.text,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
block.textWidth);
|
||||
BindLinks(&block.leaf, prepared.links);
|
||||
const auto captionTop = bottom + style.captionSkip;
|
||||
const auto captionHeight = std::max(
|
||||
block.leaf.countHeight(block.textWidth, true),
|
||||
TextLineHeight(markdown.body));
|
||||
block.textRect = QRect(
|
||||
contentLeft,
|
||||
captionTop,
|
||||
block.textWidth,
|
||||
captionHeight);
|
||||
bottom = captionTop + captionHeight;
|
||||
}
|
||||
LayoutMediaCaption(
|
||||
&block,
|
||||
prepared,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
markdown,
|
||||
contentLeft,
|
||||
bottom,
|
||||
contentWidth,
|
||||
style.captionSkip,
|
||||
&bottom);
|
||||
|
||||
block.contentRect = QRect(
|
||||
left,
|
||||
@@ -770,22 +997,21 @@ LaidOutBlock LayoutPhotoBlock(
|
||||
const auto mediaWidth = std::max(
|
||||
blockWidth - style.padding.left() - style.padding.right(),
|
||||
1);
|
||||
const auto aspectWidth = std::max(prepared.photo.width, 1);
|
||||
const auto aspectHeight = std::max(prepared.photo.height, 1);
|
||||
const auto mediaHeight = std::max(
|
||||
int((int64(mediaWidth) * aspectHeight + aspectWidth - 1) / aspectWidth),
|
||||
1);
|
||||
const auto mediaHeight = MediaHeightForWidth(
|
||||
mediaWidth,
|
||||
prepared.photo.width,
|
||||
prepared.photo.height);
|
||||
block.mediaRect = QRect(mediaLeft, mediaTop, mediaWidth, mediaHeight);
|
||||
block.visibleMediaRect = block.mediaRect;
|
||||
|
||||
if (mediaRuntime) {
|
||||
block.photoRuntime = mediaRuntime->resolvePhoto(prepared.photo.photoId);
|
||||
}
|
||||
if (block.photoRuntime) {
|
||||
const auto size = QSize(mediaWidth, mediaHeight);
|
||||
block.thumbnailImage = block.photoRuntime->thumbnail(size);
|
||||
block.fullImage = block.photoRuntime->full(size);
|
||||
}
|
||||
ResolveRuntimeImages(
|
||||
block.photoRuntime,
|
||||
QSize(mediaWidth, mediaHeight),
|
||||
&block.thumbnailImage,
|
||||
&block.fullImage);
|
||||
if (!prepared.photo.urlOverride.isEmpty()) {
|
||||
block.activation.kind = MediaActivationKind::ExternalUrl;
|
||||
block.activation.url = prepared.photo.urlOverride;
|
||||
@@ -795,28 +1021,18 @@ LaidOutBlock LayoutPhotoBlock(
|
||||
}
|
||||
|
||||
auto bottom = mediaTop + mediaHeight + style.padding.bottom();
|
||||
if (!prepared.text.text.isEmpty()) {
|
||||
block.textWidth = mediaWidth;
|
||||
SetTextLeaf(
|
||||
&block.leaf,
|
||||
markdown.body,
|
||||
prepared.text,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
block.textWidth);
|
||||
BindLinks(&block.leaf, prepared.links);
|
||||
const auto captionTop = bottom + style.captionSkip;
|
||||
const auto captionHeight = std::max(
|
||||
block.leaf.countHeight(block.textWidth, true),
|
||||
TextLineHeight(markdown.body));
|
||||
block.textRect = QRect(
|
||||
mediaLeft,
|
||||
captionTop,
|
||||
block.textWidth,
|
||||
captionHeight);
|
||||
bottom = captionTop + captionHeight;
|
||||
}
|
||||
LayoutMediaCaption(
|
||||
&block,
|
||||
prepared,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
markdown,
|
||||
mediaLeft,
|
||||
bottom,
|
||||
mediaWidth,
|
||||
style.captionSkip,
|
||||
&bottom);
|
||||
|
||||
block.contentRect = QRect(
|
||||
mediaLeft,
|
||||
@@ -827,4 +1043,454 @@ LaidOutBlock LayoutPhotoBlock(
|
||||
return block;
|
||||
}
|
||||
|
||||
LaidOutBlock LayoutVideoBlock(
|
||||
const PreparedBlock &prepared,
|
||||
std::vector<PreparedFormulaSlot> *formulas,
|
||||
InlineFormulaObjectCache *inlineFormulaObjects,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width) {
|
||||
auto block = LaidOutBlock();
|
||||
block.kind = PreparedBlockKind::Video;
|
||||
block.anchorId = prepared.anchorId;
|
||||
block.copyText = tr::lng_in_dlg_video(tr::now);
|
||||
|
||||
const auto &style = markdown.photo;
|
||||
const auto blockWidth = std::max(width, 1);
|
||||
const auto mediaLeft = left + style.padding.left();
|
||||
const auto mediaTop = top + style.padding.top();
|
||||
const auto mediaWidth = std::max(
|
||||
blockWidth - style.padding.left() - style.padding.right(),
|
||||
1);
|
||||
const auto mediaHeight = MediaHeightForWidth(
|
||||
mediaWidth,
|
||||
prepared.video.media.width,
|
||||
prepared.video.media.height);
|
||||
block.mediaRect = QRect(mediaLeft, mediaTop, mediaWidth, mediaHeight);
|
||||
block.visibleMediaRect = block.mediaRect;
|
||||
|
||||
if (mediaRuntime) {
|
||||
block.documentRuntime = mediaRuntime->resolveDocument(
|
||||
prepared.video.media.id);
|
||||
}
|
||||
ResolveRuntimeImages(
|
||||
block.documentRuntime,
|
||||
QSize(mediaWidth, mediaHeight),
|
||||
&block.thumbnailImage,
|
||||
&block.fullImage);
|
||||
if (block.documentRuntime) {
|
||||
block.activation.kind = MediaActivationKind::Document;
|
||||
block.activation.document = block.documentRuntime;
|
||||
}
|
||||
|
||||
auto bottom = mediaTop + mediaHeight + style.padding.bottom();
|
||||
LayoutMediaCaption(
|
||||
&block,
|
||||
prepared,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
markdown,
|
||||
mediaLeft,
|
||||
bottom,
|
||||
mediaWidth,
|
||||
style.captionSkip,
|
||||
&bottom);
|
||||
|
||||
block.contentRect = QRect(
|
||||
mediaLeft,
|
||||
mediaTop,
|
||||
mediaWidth,
|
||||
std::max(bottom - mediaTop, mediaHeight));
|
||||
block.outer = QRect(left, top, blockWidth, std::max(bottom - top, mediaHeight));
|
||||
return block;
|
||||
}
|
||||
|
||||
LaidOutBlock LayoutAudioBlock(
|
||||
const PreparedBlock &prepared,
|
||||
std::vector<PreparedFormulaSlot> *formulas,
|
||||
InlineFormulaObjectCache *inlineFormulaObjects,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width) {
|
||||
auto block = LaidOutBlock();
|
||||
block.kind = PreparedBlockKind::Audio;
|
||||
block.anchorId = prepared.anchorId;
|
||||
block.labelText = AudioTitleText(prepared.audio);
|
||||
block.copyText = AudioCopyText(prepared.audio);
|
||||
|
||||
const auto &card = markdown.audio;
|
||||
const auto &padding = card.padding;
|
||||
const auto &titleStyle = card.titleStyle;
|
||||
const auto &subtitleStyle = card.subtitleStyle;
|
||||
const auto subtitleText = AudioSubtitleText(prepared.audio);
|
||||
const auto blockWidth = std::max(width, 1);
|
||||
const auto contentLeft = left + padding.left();
|
||||
const auto contentWidth = std::max(
|
||||
blockWidth - padding.left() - padding.right(),
|
||||
1);
|
||||
|
||||
block.labelWidth = contentWidth;
|
||||
SetPlainTextLeaf(
|
||||
&block.labelLeaf,
|
||||
titleStyle,
|
||||
block.labelText,
|
||||
block.labelWidth);
|
||||
const auto titleHeight = LeafHeight(
|
||||
block.labelLeaf,
|
||||
titleStyle,
|
||||
block.labelWidth);
|
||||
|
||||
auto subtitleHeight = 0;
|
||||
if (!subtitleText.isEmpty()) {
|
||||
block.subtitleWidth = contentWidth;
|
||||
SetPlainTextLeaf(
|
||||
&block.subtitleLeaf,
|
||||
subtitleStyle,
|
||||
subtitleText,
|
||||
block.subtitleWidth);
|
||||
subtitleHeight = LeafHeight(
|
||||
block.subtitleLeaf,
|
||||
subtitleStyle,
|
||||
block.subtitleWidth);
|
||||
}
|
||||
const auto textSkip = subtitleHeight ? card.textSkip : 0;
|
||||
const auto textHeight = titleHeight + textSkip + subtitleHeight;
|
||||
const auto cardHeight = padding.top() + textHeight + padding.bottom();
|
||||
|
||||
block.mediaRect = QRect(left, top, blockWidth, cardHeight);
|
||||
block.visibleMediaRect = block.mediaRect;
|
||||
block.labelRect = QRect(
|
||||
contentLeft,
|
||||
top + padding.top(),
|
||||
block.labelWidth,
|
||||
titleHeight);
|
||||
if (subtitleHeight) {
|
||||
block.subtitleRect = QRect(
|
||||
contentLeft,
|
||||
block.labelRect.y() + block.labelRect.height() + textSkip,
|
||||
block.subtitleWidth,
|
||||
subtitleHeight);
|
||||
}
|
||||
block.firstLineBaseline = LeafFirstLineBaseline(
|
||||
block.labelLeaf,
|
||||
block.labelRect,
|
||||
titleStyle);
|
||||
|
||||
if (mediaRuntime) {
|
||||
block.documentRuntime = mediaRuntime->resolveDocument(
|
||||
prepared.audio.documentId);
|
||||
}
|
||||
if (block.documentRuntime) {
|
||||
block.activation.kind = MediaActivationKind::Document;
|
||||
block.activation.document = block.documentRuntime;
|
||||
}
|
||||
|
||||
auto bottom = top + cardHeight;
|
||||
LayoutMediaCaption(
|
||||
&block,
|
||||
prepared,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
markdown,
|
||||
contentLeft,
|
||||
bottom,
|
||||
contentWidth,
|
||||
card.captionSkip,
|
||||
&bottom);
|
||||
block.contentRect = QRect(left, top, blockWidth, std::max(bottom - top, cardHeight));
|
||||
block.outer = block.contentRect;
|
||||
return block;
|
||||
}
|
||||
|
||||
LaidOutBlock LayoutMapBlock(
|
||||
const PreparedBlock &prepared,
|
||||
std::vector<PreparedFormulaSlot> *formulas,
|
||||
InlineFormulaObjectCache *inlineFormulaObjects,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width) {
|
||||
auto block = LaidOutBlock();
|
||||
block.kind = PreparedBlockKind::Map;
|
||||
block.anchorId = prepared.anchorId;
|
||||
block.copyText = tr::lng_maps_point(tr::now);
|
||||
|
||||
const auto &style = markdown.photo;
|
||||
const auto blockWidth = std::max(width, 1);
|
||||
const auto mediaLeft = left + style.padding.left();
|
||||
const auto mediaTop = top + style.padding.top();
|
||||
const auto mediaWidth = std::max(
|
||||
blockWidth - style.padding.left() - style.padding.right(),
|
||||
1);
|
||||
const auto mediaHeight = MediaHeightForWidth(
|
||||
mediaWidth,
|
||||
prepared.map.width,
|
||||
prepared.map.height);
|
||||
block.mediaRect = QRect(mediaLeft, mediaTop, mediaWidth, mediaHeight);
|
||||
block.visibleMediaRect = block.mediaRect;
|
||||
|
||||
if (mediaRuntime) {
|
||||
block.mapRuntime = mediaRuntime->resolveMap(
|
||||
prepared.map.latitude,
|
||||
prepared.map.longitude,
|
||||
prepared.map.accessHash,
|
||||
QSize(mediaWidth, mediaHeight),
|
||||
prepared.map.zoom);
|
||||
}
|
||||
ResolveRuntimeImages(
|
||||
block.mapRuntime,
|
||||
QSize(mediaWidth, mediaHeight),
|
||||
&block.thumbnailImage,
|
||||
&block.fullImage);
|
||||
if (!prepared.map.url.isEmpty()) {
|
||||
block.activation.kind = MediaActivationKind::ExternalUrl;
|
||||
block.activation.url = prepared.map.url;
|
||||
}
|
||||
|
||||
auto bottom = mediaTop + mediaHeight + style.padding.bottom();
|
||||
LayoutMediaCaption(
|
||||
&block,
|
||||
prepared,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
markdown,
|
||||
mediaLeft,
|
||||
bottom,
|
||||
mediaWidth,
|
||||
style.captionSkip,
|
||||
&bottom);
|
||||
|
||||
block.contentRect = QRect(
|
||||
mediaLeft,
|
||||
mediaTop,
|
||||
mediaWidth,
|
||||
std::max(bottom - mediaTop, mediaHeight));
|
||||
block.outer = QRect(left, top, blockWidth, std::max(bottom - top, mediaHeight));
|
||||
return block;
|
||||
}
|
||||
|
||||
LaidOutBlock LayoutChannelBlock(
|
||||
const PreparedBlock &prepared,
|
||||
std::vector<PreparedFormulaSlot> *formulas,
|
||||
InlineFormulaObjectCache *inlineFormulaObjects,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width) {
|
||||
auto block = LaidOutBlock();
|
||||
block.kind = PreparedBlockKind::Channel;
|
||||
block.anchorId = prepared.anchorId;
|
||||
block.labelText = prepared.channel.title;
|
||||
block.copyText = ChannelCopyText(prepared.channel);
|
||||
|
||||
const auto &card = markdown.channel;
|
||||
const auto &padding = card.padding;
|
||||
const auto &button = card.button;
|
||||
const auto &buttonPadding = button.padding;
|
||||
const auto &titleStyle = card.titleStyle;
|
||||
const auto &subtitleStyle = card.subtitleStyle;
|
||||
const auto &actionStyle = button.textStyle;
|
||||
const auto subtitleText = ChannelSubtitleText(prepared.channel);
|
||||
const auto blockWidth = std::max(width, 1);
|
||||
const auto contentLeft = left + padding.left();
|
||||
const auto contentWidth = std::max(
|
||||
blockWidth - padding.left() - padding.right(),
|
||||
1);
|
||||
|
||||
if (mediaRuntime) {
|
||||
block.channelRuntime = mediaRuntime->resolveChannel(
|
||||
prepared.channel.channelId,
|
||||
prepared.channel.username);
|
||||
}
|
||||
const auto joinVisible = block.channelRuntime
|
||||
&& block.channelRuntime->joinVisible();
|
||||
if (block.channelRuntime) {
|
||||
block.activation.kind = MediaActivationKind::OpenChannel;
|
||||
block.activation.channel = block.channelRuntime;
|
||||
}
|
||||
if (joinVisible) {
|
||||
block.actionActivation.kind = MediaActivationKind::JoinChannel;
|
||||
block.actionActivation.channel = block.channelRuntime;
|
||||
}
|
||||
|
||||
auto actionTextHeight = 0;
|
||||
auto actionOuterWidth = 0;
|
||||
auto actionOuterHeight = 0;
|
||||
if (joinVisible) {
|
||||
SetPlainTextLeaf(
|
||||
&block.actionLeaf,
|
||||
actionStyle,
|
||||
tr::lng_iv_join_channel(tr::now),
|
||||
contentWidth);
|
||||
block.actionWidth = std::max(block.actionLeaf.maxWidth(), 1);
|
||||
actionTextHeight = LeafHeight(
|
||||
block.actionLeaf,
|
||||
actionStyle,
|
||||
block.actionWidth);
|
||||
actionOuterWidth = block.actionWidth
|
||||
+ buttonPadding.left()
|
||||
+ buttonPadding.right();
|
||||
actionOuterHeight = actionTextHeight
|
||||
+ buttonPadding.top()
|
||||
+ buttonPadding.bottom();
|
||||
}
|
||||
|
||||
block.labelWidth = std::max(
|
||||
contentWidth
|
||||
- (joinVisible ? (actionOuterWidth + card.buttonSkip) : 0),
|
||||
1);
|
||||
SetPlainTextLeaf(
|
||||
&block.labelLeaf,
|
||||
titleStyle,
|
||||
block.labelText,
|
||||
block.labelWidth);
|
||||
const auto titleHeight = LeafHeight(
|
||||
block.labelLeaf,
|
||||
titleStyle,
|
||||
block.labelWidth);
|
||||
|
||||
auto subtitleHeight = 0;
|
||||
if (!subtitleText.isEmpty()) {
|
||||
block.subtitleWidth = block.labelWidth;
|
||||
SetPlainTextLeaf(
|
||||
&block.subtitleLeaf,
|
||||
subtitleStyle,
|
||||
subtitleText,
|
||||
block.subtitleWidth);
|
||||
subtitleHeight = LeafHeight(
|
||||
block.subtitleLeaf,
|
||||
subtitleStyle,
|
||||
block.subtitleWidth);
|
||||
}
|
||||
const auto textSkip = subtitleHeight ? card.textSkip : 0;
|
||||
const auto textHeight = titleHeight + textSkip + subtitleHeight;
|
||||
const auto cardContentHeight = std::max(textHeight, actionOuterHeight);
|
||||
const auto cardHeight = padding.top() + cardContentHeight + padding.bottom();
|
||||
|
||||
block.mediaRect = QRect(left, top, blockWidth, cardHeight);
|
||||
block.visibleMediaRect = block.mediaRect;
|
||||
|
||||
const auto textTop = top + padding.top()
|
||||
+ std::max((cardContentHeight - textHeight) / 2, 0);
|
||||
block.labelRect = QRect(
|
||||
contentLeft,
|
||||
textTop,
|
||||
block.labelWidth,
|
||||
titleHeight);
|
||||
if (subtitleHeight) {
|
||||
block.subtitleRect = QRect(
|
||||
contentLeft,
|
||||
block.labelRect.y() + block.labelRect.height() + textSkip,
|
||||
block.subtitleWidth,
|
||||
subtitleHeight);
|
||||
}
|
||||
if (joinVisible) {
|
||||
block.actionRect = QRect(
|
||||
left + blockWidth - padding.right() - actionOuterWidth,
|
||||
top + padding.top()
|
||||
+ std::max((cardContentHeight - actionOuterHeight) / 2, 0),
|
||||
actionOuterWidth,
|
||||
actionOuterHeight);
|
||||
}
|
||||
block.firstLineBaseline = LeafFirstLineBaseline(
|
||||
block.labelLeaf,
|
||||
block.labelRect,
|
||||
titleStyle);
|
||||
block.contentRect = block.mediaRect;
|
||||
block.outer = block.mediaRect;
|
||||
return block;
|
||||
}
|
||||
|
||||
LaidOutBlock LayoutGroupedMediaBlock(
|
||||
const PreparedBlock &prepared,
|
||||
const std::vector<PreparedFormulaSlot> *formulas,
|
||||
InlineFormulaObjectCache *inlineFormulaObjects,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width) {
|
||||
auto block = LaidOutBlock();
|
||||
block.kind = PreparedBlockKind::GroupedMedia;
|
||||
block.anchorId = prepared.anchorId;
|
||||
block.copyText = GroupedMediaCopyText(prepared.groupedMedia);
|
||||
|
||||
const auto &style = markdown.groupedMedia;
|
||||
const auto blockWidth = std::max(width, 1);
|
||||
const auto mediaLeft = left + style.padding.left();
|
||||
const auto mediaTop = top + style.padding.top();
|
||||
const auto mediaWidth = std::max(
|
||||
blockWidth - style.padding.left() - style.padding.right(),
|
||||
1);
|
||||
|
||||
auto sizes = std::vector<QSize>();
|
||||
sizes.reserve(prepared.groupedMedia.items.size());
|
||||
for (const auto &item : prepared.groupedMedia.items) {
|
||||
sizes.push_back(QSize(
|
||||
std::max(item.media.width, 1),
|
||||
std::max(item.media.height, 1)));
|
||||
}
|
||||
auto layout = Ui::LayoutMediaGroup(
|
||||
sizes,
|
||||
mediaWidth,
|
||||
GroupedMediaMinWidth(mediaWidth, style.itemSkip),
|
||||
style.itemSkip);
|
||||
block.groupedMediaItems.reserve(layout.size());
|
||||
for (auto i = 0, count = std::min(
|
||||
int(layout.size()),
|
||||
int(prepared.groupedMedia.items.size())); i != count; ++i) {
|
||||
auto item = LaidOutGroupedMediaItem();
|
||||
ResolveGroupedMediaItemLayout(
|
||||
&item,
|
||||
prepared.groupedMedia.items[i],
|
||||
mediaRuntime,
|
||||
layout[i].geometry.translated(mediaLeft, mediaTop));
|
||||
block.groupedMediaItems.push_back(std::move(item));
|
||||
}
|
||||
const auto mediaHeight = GroupedMediaLayoutHeight(layout);
|
||||
const auto laidOutWidth = GroupedMediaLayoutWidth(layout);
|
||||
block.mediaRect = QRect(
|
||||
mediaLeft,
|
||||
mediaTop,
|
||||
std::max(laidOutWidth, 1),
|
||||
std::max(mediaHeight, 1));
|
||||
block.visibleMediaRect = block.mediaRect;
|
||||
|
||||
auto bottom = block.mediaRect.y() + block.mediaRect.height()
|
||||
+ style.padding.bottom();
|
||||
LayoutMediaCaption(
|
||||
&block,
|
||||
prepared,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
markdown,
|
||||
mediaLeft,
|
||||
bottom,
|
||||
block.mediaRect.width(),
|
||||
style.captionSkip,
|
||||
&bottom);
|
||||
|
||||
block.contentRect = QRect(
|
||||
mediaLeft,
|
||||
mediaTop,
|
||||
block.mediaRect.width(),
|
||||
std::max(bottom - mediaTop, block.mediaRect.height()));
|
||||
block.outer = QRect(
|
||||
left,
|
||||
top,
|
||||
blockWidth,
|
||||
std::max(bottom - top, block.mediaRect.height()));
|
||||
return block;
|
||||
}
|
||||
|
||||
} // namespace Iv::Markdown
|
||||
|
||||
@@ -49,10 +49,25 @@ struct LaidOutTableRow {
|
||||
bool header = false;
|
||||
};
|
||||
|
||||
struct LaidOutGroupedMediaItem {
|
||||
PreparedMediaItemKind kind = PreparedMediaItemKind::Photo;
|
||||
QString copyText;
|
||||
QRect rect;
|
||||
std::shared_ptr<PhotoRuntime> photoRuntime;
|
||||
std::shared_ptr<DocumentRuntime> documentRuntime;
|
||||
std::shared_ptr<Ui::DynamicImage> thumbnailImage;
|
||||
std::shared_ptr<Ui::DynamicImage> fullImage;
|
||||
MediaActivation activation;
|
||||
mutable bool thumbnailSubscribed = false;
|
||||
mutable bool fullSubscribed = false;
|
||||
};
|
||||
|
||||
struct LaidOutBlock {
|
||||
PreparedBlockKind kind = PreparedBlockKind::Paragraph;
|
||||
Ui::Text::String leaf;
|
||||
Ui::Text::String labelLeaf;
|
||||
Ui::Text::String subtitleLeaf;
|
||||
Ui::Text::String actionLeaf;
|
||||
Ui::Text::String marker;
|
||||
Ui::Text::String fallbackLeaf;
|
||||
QString copyText;
|
||||
@@ -60,6 +75,7 @@ struct LaidOutBlock {
|
||||
QString codeLanguage;
|
||||
Spellchecker::HighlightProcessId syntaxHighlightProcessId = 0;
|
||||
std::vector<LaidOutBlock> children;
|
||||
std::vector<LaidOutGroupedMediaItem> groupedMediaItems;
|
||||
std::vector<LaidOutTableRow> tableRows;
|
||||
std::vector<int> tableColumnWidths;
|
||||
QRect outer;
|
||||
@@ -68,6 +84,8 @@ struct LaidOutBlock {
|
||||
QRect iconRect;
|
||||
QRect textRect;
|
||||
QRect labelRect;
|
||||
QRect subtitleRect;
|
||||
QRect actionRect;
|
||||
QRect markerRect;
|
||||
QRect contentRect;
|
||||
QRect formulaRect;
|
||||
@@ -80,6 +98,8 @@ struct LaidOutBlock {
|
||||
QString anchorId;
|
||||
int textWidth = 0;
|
||||
int labelWidth = 0;
|
||||
int subtitleWidth = 0;
|
||||
int actionWidth = 0;
|
||||
int markerWidth = 0;
|
||||
int firstLineBaseline = -1;
|
||||
int headingLevel = 0;
|
||||
@@ -94,9 +114,13 @@ struct LaidOutBlock {
|
||||
int segmentIndex = -1;
|
||||
int secondarySegmentIndex = -1;
|
||||
std::shared_ptr<PhotoRuntime> photoRuntime;
|
||||
std::shared_ptr<DocumentRuntime> documentRuntime;
|
||||
std::shared_ptr<MapRuntime> mapRuntime;
|
||||
std::shared_ptr<ChannelRuntime> channelRuntime;
|
||||
std::shared_ptr<Ui::DynamicImage> thumbnailImage;
|
||||
std::shared_ptr<Ui::DynamicImage> fullImage;
|
||||
MediaActivation activation;
|
||||
MediaActivation actionActivation;
|
||||
mutable bool thumbnailSubscribed = false;
|
||||
mutable bool fullSubscribed = false;
|
||||
mutable QImage colorizedFormulaImage;
|
||||
@@ -211,5 +235,50 @@ void RepopulateCodeBlockLeaf(
|
||||
int left,
|
||||
int top,
|
||||
int width);
|
||||
[[nodiscard]] LaidOutBlock LayoutVideoBlock(
|
||||
const PreparedBlock &prepared,
|
||||
std::vector<PreparedFormulaSlot> *formulas,
|
||||
InlineFormulaObjectCache *inlineFormulaObjects,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width);
|
||||
[[nodiscard]] LaidOutBlock LayoutAudioBlock(
|
||||
const PreparedBlock &prepared,
|
||||
std::vector<PreparedFormulaSlot> *formulas,
|
||||
InlineFormulaObjectCache *inlineFormulaObjects,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width);
|
||||
[[nodiscard]] LaidOutBlock LayoutMapBlock(
|
||||
const PreparedBlock &prepared,
|
||||
std::vector<PreparedFormulaSlot> *formulas,
|
||||
InlineFormulaObjectCache *inlineFormulaObjects,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width);
|
||||
[[nodiscard]] LaidOutBlock LayoutChannelBlock(
|
||||
const PreparedBlock &prepared,
|
||||
std::vector<PreparedFormulaSlot> *formulas,
|
||||
InlineFormulaObjectCache *inlineFormulaObjects,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width);
|
||||
[[nodiscard]] LaidOutBlock LayoutGroupedMediaBlock(
|
||||
const PreparedBlock &prepared,
|
||||
const std::vector<PreparedFormulaSlot> *formulas,
|
||||
InlineFormulaObjectCache *inlineFormulaObjects,
|
||||
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
||||
const style::Markdown &markdown,
|
||||
int left,
|
||||
int top,
|
||||
int width);
|
||||
|
||||
} // namespace Iv::Markdown
|
||||
|
||||
@@ -57,6 +57,11 @@ namespace {
|
||||
case PreparedBlockKind::DisplayMath:
|
||||
case PreparedBlockKind::Table:
|
||||
case PreparedBlockKind::Photo:
|
||||
case PreparedBlockKind::Video:
|
||||
case PreparedBlockKind::Audio:
|
||||
case PreparedBlockKind::Map:
|
||||
case PreparedBlockKind::Channel:
|
||||
case PreparedBlockKind::GroupedMedia:
|
||||
case PreparedBlockKind::Placeholder:
|
||||
case PreparedBlockKind::Details:
|
||||
return false;
|
||||
@@ -554,6 +559,46 @@ namespace {
|
||||
left,
|
||||
top,
|
||||
width);
|
||||
case PreparedBlockKind::Video:
|
||||
return LayoutVideoBlock(
|
||||
prepared,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
markdown,
|
||||
left,
|
||||
top,
|
||||
width);
|
||||
case PreparedBlockKind::Audio:
|
||||
return LayoutAudioBlock(
|
||||
prepared,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
markdown,
|
||||
left,
|
||||
top,
|
||||
width);
|
||||
case PreparedBlockKind::Map:
|
||||
return LayoutMapBlock(
|
||||
prepared,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
markdown,
|
||||
left,
|
||||
top,
|
||||
width);
|
||||
case PreparedBlockKind::Channel:
|
||||
return LayoutChannelBlock(
|
||||
prepared,
|
||||
formulas,
|
||||
inlineFormulaObjects,
|
||||
mediaRuntime,
|
||||
markdown,
|
||||
left,
|
||||
top,
|
||||
width);
|
||||
case PreparedBlockKind::Placeholder:
|
||||
return LayoutPlaceholderBlock(
|
||||
prepared,
|
||||
|
||||
@@ -459,7 +459,181 @@ void PaintPhotoProgress(
|
||||
-int(std::round(360. * 16. * std::clamp(progress, 0., 1.))));
|
||||
}
|
||||
|
||||
void PaintPhotoBlock(
|
||||
[[nodiscard]] QPainterPath RoundedRectPath(QRect rect, int radius) {
|
||||
auto path = QPainterPath();
|
||||
path.addRoundedRect(QRectF(rect), radius, radius);
|
||||
return path;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool MediaLoading(const LaidOutBlock &block) {
|
||||
if (block.photoRuntime) {
|
||||
return block.photoRuntime->loading();
|
||||
} else if (block.documentRuntime) {
|
||||
return block.documentRuntime->loading();
|
||||
} else if (block.mapRuntime) {
|
||||
return block.mapRuntime->loading();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] double MediaProgress(const LaidOutBlock &block) {
|
||||
if (block.photoRuntime) {
|
||||
return block.photoRuntime->progress();
|
||||
} else if (block.documentRuntime) {
|
||||
return block.documentRuntime->progress();
|
||||
} else if (block.mapRuntime) {
|
||||
return block.mapRuntime->progress();
|
||||
}
|
||||
return 0.;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool MediaLoading(const LaidOutGroupedMediaItem &item) {
|
||||
if (item.photoRuntime) {
|
||||
return item.photoRuntime->loading();
|
||||
} else if (item.documentRuntime) {
|
||||
return item.documentRuntime->loading();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] double MediaProgress(const LaidOutGroupedMediaItem &item) {
|
||||
if (item.photoRuntime) {
|
||||
return item.photoRuntime->progress();
|
||||
} else if (item.documentRuntime) {
|
||||
return item.documentRuntime->progress();
|
||||
}
|
||||
return 0.;
|
||||
}
|
||||
|
||||
void PaintMediaCaption(
|
||||
Painter &p,
|
||||
const LaidOutBlock &block,
|
||||
const style::Markdown &markdown,
|
||||
const MarkdownArticlePaintCaches &caches,
|
||||
const PaintSelectionState &selectionState,
|
||||
QRect clip) {
|
||||
if (block.textRect.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
p.setPen(markdown.textColor->c);
|
||||
PaintTextLeaf(
|
||||
p,
|
||||
block.leaf,
|
||||
caches,
|
||||
block.textRect,
|
||||
block.textWidth,
|
||||
clip,
|
||||
style::al_left,
|
||||
TextSelectionForSegmentIndex(
|
||||
selectionState,
|
||||
block.secondarySegmentIndex));
|
||||
}
|
||||
|
||||
void PaintImageBackedMedia(
|
||||
Painter &p,
|
||||
QRect rect,
|
||||
const QString ©Text,
|
||||
const std::shared_ptr<Ui::DynamicImage> &thumbnailImage,
|
||||
const std::shared_ptr<Ui::DynamicImage> &fullImage,
|
||||
bool *thumbnailSubscribed,
|
||||
bool *fullSubscribed,
|
||||
bool loading,
|
||||
double progress,
|
||||
const style::Markdown &markdown,
|
||||
const MarkdownArticlePaintCaches &caches,
|
||||
bool selected,
|
||||
QRect clip) {
|
||||
const auto visible = clip.intersected(rect);
|
||||
if (!visible.isEmpty()) {
|
||||
p.save();
|
||||
p.setClipRect(visible);
|
||||
p.fillRect(rect, st::windowBgOver->c);
|
||||
SubscribeDynamicImage(
|
||||
thumbnailImage,
|
||||
caches.repaint,
|
||||
thumbnailSubscribed);
|
||||
SubscribeDynamicImage(
|
||||
fullImage,
|
||||
caches.repaint,
|
||||
fullSubscribed);
|
||||
const auto paintedThumb = PaintDynamicImage(
|
||||
p,
|
||||
thumbnailImage,
|
||||
rect);
|
||||
const auto paintedFull = PaintDynamicImage(
|
||||
p,
|
||||
fullImage,
|
||||
rect);
|
||||
if (!paintedThumb && !paintedFull) {
|
||||
p.setPen(st::windowSubTextFg->c);
|
||||
p.drawText(
|
||||
rect,
|
||||
Qt::AlignCenter | Qt::TextWordWrap,
|
||||
copyText);
|
||||
}
|
||||
if (loading) {
|
||||
PaintPhotoProgress(
|
||||
p,
|
||||
rect,
|
||||
markdown.photo,
|
||||
progress);
|
||||
}
|
||||
if (selected) {
|
||||
p.fillRect(rect, p.textPalette().selectOverlay);
|
||||
}
|
||||
p.restore();
|
||||
}
|
||||
}
|
||||
|
||||
void PaintImageBackedMediaBlock(
|
||||
Painter &p,
|
||||
const LaidOutBlock &block,
|
||||
const style::Markdown &markdown,
|
||||
const MarkdownArticlePaintCaches &caches,
|
||||
const PaintSelectionState &selectionState,
|
||||
QRect clip) {
|
||||
PaintImageBackedMedia(
|
||||
p,
|
||||
block.mediaRect,
|
||||
block.copyText,
|
||||
block.thumbnailImage,
|
||||
block.fullImage,
|
||||
&block.thumbnailSubscribed,
|
||||
&block.fullSubscribed,
|
||||
MediaLoading(block),
|
||||
MediaProgress(block),
|
||||
markdown,
|
||||
caches,
|
||||
block.segmentIndex >= 0
|
||||
&& WholeSegmentSelected(selectionState, block.segmentIndex),
|
||||
clip);
|
||||
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
|
||||
}
|
||||
|
||||
void PaintCardSurface(
|
||||
Painter &p,
|
||||
QRect rect,
|
||||
int border,
|
||||
const style::color &borderFg,
|
||||
const style::color &bg,
|
||||
int radius) {
|
||||
if (rect.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto half = border / 2.;
|
||||
const auto inner = QRectF(rect).marginsRemoved({
|
||||
half,
|
||||
half,
|
||||
half,
|
||||
half,
|
||||
});
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(QPen(borderFg->c, border));
|
||||
p.setBrush(bg->c);
|
||||
p.drawRoundedRect(inner, radius, radius);
|
||||
}
|
||||
|
||||
void PaintAudioBlock(
|
||||
Painter &p,
|
||||
const LaidOutBlock &block,
|
||||
const style::Markdown &markdown,
|
||||
@@ -468,38 +642,33 @@ void PaintPhotoBlock(
|
||||
QRect clip) {
|
||||
const auto visible = clip.intersected(block.visibleMediaRect);
|
||||
if (!visible.isEmpty()) {
|
||||
const auto &style = markdown.audio;
|
||||
p.save();
|
||||
p.setClipRect(visible);
|
||||
p.fillRect(block.mediaRect, st::windowBgOver->c);
|
||||
SubscribeDynamicImage(
|
||||
block.thumbnailImage,
|
||||
caches.repaint,
|
||||
&block.thumbnailSubscribed);
|
||||
SubscribeDynamicImage(
|
||||
block.fullImage,
|
||||
caches.repaint,
|
||||
&block.fullSubscribed);
|
||||
const auto paintedThumb = PaintDynamicImage(
|
||||
PaintCardSurface(
|
||||
p,
|
||||
block.thumbnailImage,
|
||||
block.mediaRect);
|
||||
const auto paintedFull = PaintDynamicImage(
|
||||
block.mediaRect,
|
||||
style.border,
|
||||
style.borderFg,
|
||||
style.bg,
|
||||
style.radius);
|
||||
p.setPen(style.titleFg->c);
|
||||
PaintTextLeaf(
|
||||
p,
|
||||
block.fullImage,
|
||||
block.mediaRect);
|
||||
if (!paintedThumb && !paintedFull) {
|
||||
p.setPen(st::windowSubTextFg->c);
|
||||
p.drawText(
|
||||
block.mediaRect,
|
||||
Qt::AlignCenter | Qt::TextWordWrap,
|
||||
block.copyText);
|
||||
}
|
||||
if (block.photoRuntime && block.photoRuntime->loading()) {
|
||||
PaintPhotoProgress(
|
||||
block.labelLeaf,
|
||||
caches,
|
||||
block.labelRect,
|
||||
block.labelWidth,
|
||||
visible);
|
||||
if (!block.subtitleRect.isEmpty()) {
|
||||
p.setPen(style.subtitleFg->c);
|
||||
PaintTextLeaf(
|
||||
p,
|
||||
block.mediaRect,
|
||||
markdown.photo,
|
||||
block.photoRuntime->progress());
|
||||
block.subtitleLeaf,
|
||||
caches,
|
||||
block.subtitleRect,
|
||||
block.subtitleWidth,
|
||||
visible);
|
||||
}
|
||||
if (block.segmentIndex >= 0
|
||||
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
|
||||
@@ -507,20 +676,171 @@ void PaintPhotoBlock(
|
||||
}
|
||||
p.restore();
|
||||
}
|
||||
if (!block.textRect.isEmpty()) {
|
||||
p.setPen(markdown.textColor->c);
|
||||
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
|
||||
}
|
||||
|
||||
void PaintChannelBlock(
|
||||
Painter &p,
|
||||
const LaidOutBlock &block,
|
||||
const style::Markdown &markdown,
|
||||
const MarkdownArticlePaintCaches &caches,
|
||||
const PaintSelectionState &selectionState,
|
||||
QRect clip) {
|
||||
const auto visible = clip.intersected(block.visibleMediaRect);
|
||||
if (!visible.isEmpty()) {
|
||||
const auto &style = markdown.channel;
|
||||
const auto &button = style.button;
|
||||
p.save();
|
||||
p.setClipRect(visible);
|
||||
PaintCardSurface(
|
||||
p,
|
||||
block.mediaRect,
|
||||
style.border,
|
||||
style.borderFg,
|
||||
style.bg,
|
||||
style.radius);
|
||||
p.setPen(style.titleFg->c);
|
||||
PaintTextLeaf(
|
||||
p,
|
||||
block.leaf,
|
||||
block.labelLeaf,
|
||||
caches,
|
||||
block.textRect,
|
||||
block.textWidth,
|
||||
clip,
|
||||
style::al_left,
|
||||
TextSelectionForSegmentIndex(
|
||||
selectionState,
|
||||
block.secondarySegmentIndex));
|
||||
block.labelRect,
|
||||
block.labelWidth,
|
||||
visible);
|
||||
if (!block.subtitleRect.isEmpty()) {
|
||||
p.setPen(style.subtitleFg->c);
|
||||
PaintTextLeaf(
|
||||
p,
|
||||
block.subtitleLeaf,
|
||||
caches,
|
||||
block.subtitleRect,
|
||||
block.subtitleWidth,
|
||||
visible);
|
||||
}
|
||||
if (block.channelRuntime
|
||||
&& block.channelRuntime->joinVisible()
|
||||
&& !block.actionRect.isEmpty()) {
|
||||
const auto innerRect = block.actionRect.marginsRemoved(button.padding);
|
||||
const auto half = button.border / 2.;
|
||||
const auto outer = QRectF(block.actionRect).marginsRemoved({
|
||||
half,
|
||||
half,
|
||||
half,
|
||||
half,
|
||||
});
|
||||
{
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(QPen(button.borderFg->c, button.border));
|
||||
p.setBrush(button.bg->c);
|
||||
p.drawRoundedRect(outer, button.radius, button.radius);
|
||||
}
|
||||
p.setPen(button.textFg->c);
|
||||
PaintTextLeaf(
|
||||
p,
|
||||
block.actionLeaf,
|
||||
caches,
|
||||
innerRect,
|
||||
block.actionWidth,
|
||||
visible,
|
||||
style::al_center);
|
||||
}
|
||||
if (block.segmentIndex >= 0
|
||||
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
|
||||
p.fillRect(block.visibleMediaRect, p.textPalette().selectOverlay);
|
||||
}
|
||||
p.restore();
|
||||
}
|
||||
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
|
||||
}
|
||||
|
||||
void PaintPhotoBlock(
|
||||
Painter &p,
|
||||
const LaidOutBlock &block,
|
||||
const style::Markdown &markdown,
|
||||
const MarkdownArticlePaintCaches &caches,
|
||||
const PaintSelectionState &selectionState,
|
||||
QRect clip) {
|
||||
PaintImageBackedMediaBlock(
|
||||
p,
|
||||
block,
|
||||
markdown,
|
||||
caches,
|
||||
selectionState,
|
||||
clip);
|
||||
}
|
||||
|
||||
void PaintVideoBlock(
|
||||
Painter &p,
|
||||
const LaidOutBlock &block,
|
||||
const style::Markdown &markdown,
|
||||
const MarkdownArticlePaintCaches &caches,
|
||||
const PaintSelectionState &selectionState,
|
||||
QRect clip) {
|
||||
PaintImageBackedMediaBlock(
|
||||
p,
|
||||
block,
|
||||
markdown,
|
||||
caches,
|
||||
selectionState,
|
||||
clip);
|
||||
}
|
||||
|
||||
void PaintMapBlock(
|
||||
Painter &p,
|
||||
const LaidOutBlock &block,
|
||||
const style::Markdown &markdown,
|
||||
const MarkdownArticlePaintCaches &caches,
|
||||
const PaintSelectionState &selectionState,
|
||||
QRect clip) {
|
||||
PaintImageBackedMediaBlock(
|
||||
p,
|
||||
block,
|
||||
markdown,
|
||||
caches,
|
||||
selectionState,
|
||||
clip);
|
||||
}
|
||||
|
||||
void PaintGroupedMediaBlock(
|
||||
Painter &p,
|
||||
const LaidOutBlock &block,
|
||||
const style::Markdown &markdown,
|
||||
const MarkdownArticlePaintCaches &caches,
|
||||
const PaintSelectionState &selectionState,
|
||||
QRect clip) {
|
||||
const auto selected = (block.segmentIndex >= 0)
|
||||
&& WholeSegmentSelected(selectionState, block.segmentIndex);
|
||||
const auto visible = clip.intersected(block.visibleMediaRect);
|
||||
if (!visible.isEmpty()) {
|
||||
const auto &style = markdown.groupedMedia;
|
||||
p.save();
|
||||
p.setClipRect(visible);
|
||||
const auto path = RoundedRectPath(block.mediaRect, style.radius);
|
||||
p.setClipPath(path, Qt::IntersectClip);
|
||||
for (const auto &item : block.groupedMediaItems) {
|
||||
PaintImageBackedMedia(
|
||||
p,
|
||||
item.rect,
|
||||
item.copyText,
|
||||
item.thumbnailImage,
|
||||
item.fullImage,
|
||||
&item.thumbnailSubscribed,
|
||||
&item.fullSubscribed,
|
||||
MediaLoading(item),
|
||||
MediaProgress(item),
|
||||
markdown,
|
||||
caches,
|
||||
false,
|
||||
clip);
|
||||
}
|
||||
if (selected) {
|
||||
auto overlay = p.textPalette().selectOverlay->c;
|
||||
overlay.setAlphaF(std::clamp(style.overlayOpacity, 0., 1.));
|
||||
p.fillPath(path, overlay);
|
||||
}
|
||||
p.restore();
|
||||
}
|
||||
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
|
||||
}
|
||||
|
||||
void PaintDetailsBlock(
|
||||
@@ -748,6 +1068,42 @@ void PaintBlock(
|
||||
selectionState,
|
||||
clip);
|
||||
break;
|
||||
case PreparedBlockKind::Video:
|
||||
PaintVideoBlock(
|
||||
p,
|
||||
block,
|
||||
markdown,
|
||||
caches,
|
||||
selectionState,
|
||||
clip);
|
||||
break;
|
||||
case PreparedBlockKind::Audio:
|
||||
PaintAudioBlock(
|
||||
p,
|
||||
block,
|
||||
markdown,
|
||||
caches,
|
||||
selectionState,
|
||||
clip);
|
||||
break;
|
||||
case PreparedBlockKind::Map:
|
||||
PaintMapBlock(
|
||||
p,
|
||||
block,
|
||||
markdown,
|
||||
caches,
|
||||
selectionState,
|
||||
clip);
|
||||
break;
|
||||
case PreparedBlockKind::Channel:
|
||||
PaintChannelBlock(
|
||||
p,
|
||||
block,
|
||||
markdown,
|
||||
caches,
|
||||
selectionState,
|
||||
clip);
|
||||
break;
|
||||
case PreparedBlockKind::Placeholder:
|
||||
PaintPlaceholderBlock(
|
||||
p,
|
||||
@@ -757,6 +1113,15 @@ void PaintBlock(
|
||||
selectionState,
|
||||
clip);
|
||||
break;
|
||||
case PreparedBlockKind::GroupedMedia:
|
||||
PaintGroupedMediaBlock(
|
||||
p,
|
||||
block,
|
||||
markdown,
|
||||
caches,
|
||||
selectionState,
|
||||
clip);
|
||||
break;
|
||||
case PreparedBlockKind::Details:
|
||||
PaintDetailsBlock(
|
||||
p,
|
||||
|
||||
@@ -101,18 +101,31 @@ const auto kPhotoCopyLabel = u"Photo"_q;
|
||||
[[nodiscard]] TextForMimeData CopyTextForMediaBlock(
|
||||
const QString &label,
|
||||
const Ui::Text::String &captionLeaf) {
|
||||
auto result = TextForMimeData::Simple(label);
|
||||
auto result = label.isEmpty()
|
||||
? TextForMimeData()
|
||||
: TextForMimeData::Simple(label);
|
||||
if (!captionLeaf.isEmpty()) {
|
||||
result.append(u"\n"_q);
|
||||
if (!result.empty()) {
|
||||
result.append(u"\n"_q);
|
||||
}
|
||||
result.append(captionLeaf.toTextForMimeData());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] TextForMimeData CopyTextForSingleMediaBlock(
|
||||
const LaidOutBlock &block,
|
||||
const QString &fallback = QString()) {
|
||||
const auto label = !block.copyText.isEmpty()
|
||||
? block.copyText
|
||||
: !block.labelText.isEmpty()
|
||||
? block.labelText
|
||||
: fallback;
|
||||
return CopyTextForMediaBlock(label, block.leaf);
|
||||
}
|
||||
|
||||
[[nodiscard]] TextForMimeData CopyTextForPhotoBlock(const LaidOutBlock &block) {
|
||||
return CopyTextForMediaBlock(
|
||||
block.copyText.isEmpty() ? kPhotoCopyLabel : block.copyText,
|
||||
block.leaf);
|
||||
return CopyTextForSingleMediaBlock(block, kPhotoCopyLabel);
|
||||
}
|
||||
|
||||
[[nodiscard]] TextForMimeData CopyTextForPlaceholderBlock(
|
||||
@@ -350,11 +363,18 @@ void CollectSelectableSegments(
|
||||
}
|
||||
} break;
|
||||
case PreparedBlockKind::Placeholder:
|
||||
case PreparedBlockKind::Photo: {
|
||||
case PreparedBlockKind::Photo:
|
||||
case PreparedBlockKind::Video:
|
||||
case PreparedBlockKind::Audio:
|
||||
case PreparedBlockKind::Map:
|
||||
case PreparedBlockKind::Channel:
|
||||
case PreparedBlockKind::GroupedMedia: {
|
||||
auto segment = SelectableSegment();
|
||||
segment.kind = (block.kind == PreparedBlockKind::Photo)
|
||||
? SelectableSegmentKind::Photo
|
||||
: SelectableSegmentKind::Placeholder;
|
||||
: (block.kind == PreparedBlockKind::Placeholder)
|
||||
? SelectableSegmentKind::Placeholder
|
||||
: SelectableSegmentKind::Media;
|
||||
segment.block = █
|
||||
segment.outerRect = block.mediaRect;
|
||||
segment.length = 1;
|
||||
@@ -541,6 +561,10 @@ TextForMimeData TextForSegment(
|
||||
return segment.block
|
||||
? CopyTextForPhotoBlock(*segment.block)
|
||||
: TextForMimeData();
|
||||
case SelectableSegmentKind::Media:
|
||||
return segment.block
|
||||
? CopyTextForSingleMediaBlock(*segment.block)
|
||||
: TextForMimeData();
|
||||
}
|
||||
return TextForMimeData();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ enum class SelectableSegmentKind {
|
||||
Table,
|
||||
Placeholder,
|
||||
Photo,
|
||||
Media,
|
||||
};
|
||||
|
||||
struct SelectableSegment {
|
||||
|
||||
@@ -932,9 +932,9 @@ Ui::Text::CustomEmojiSemantics InlineIvImageObject::semantics() {
|
||||
|
||||
void InlineIvImageObject::paint(QPainter &p, const Context &context) {
|
||||
if (_image) {
|
||||
if (!_subscribed && _repaint) {
|
||||
if (!_subscribed) {
|
||||
_subscribed = true;
|
||||
_image->subscribeToUpdates(_repaint);
|
||||
_image->subscribeToUpdates(_repaint ? _repaint : [] {});
|
||||
}
|
||||
if (const auto image = _image->image(std::max(_width, _height));
|
||||
!image.isNull()) {
|
||||
|
||||
@@ -11,6 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QVariant>
|
||||
|
||||
#include <rpl/never.h>
|
||||
#include <rpl/producer.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
@@ -39,6 +42,42 @@ public:
|
||||
virtual void open(Qt::MouseButton button) const = 0;
|
||||
};
|
||||
|
||||
class DocumentRuntime {
|
||||
public:
|
||||
virtual ~DocumentRuntime() = default;
|
||||
|
||||
[[nodiscard]] virtual std::shared_ptr<Ui::DynamicImage> thumbnail(
|
||||
QSize size) const = 0;
|
||||
[[nodiscard]] virtual std::shared_ptr<Ui::DynamicImage> full(
|
||||
QSize size) const = 0;
|
||||
[[nodiscard]] virtual bool loaded() const = 0;
|
||||
[[nodiscard]] virtual bool loading() const = 0;
|
||||
[[nodiscard]] virtual double progress() const = 0;
|
||||
virtual void open(Qt::MouseButton button) const = 0;
|
||||
};
|
||||
|
||||
class MapRuntime {
|
||||
public:
|
||||
virtual ~MapRuntime() = default;
|
||||
|
||||
[[nodiscard]] virtual std::shared_ptr<Ui::DynamicImage> thumbnail(
|
||||
QSize size) const = 0;
|
||||
[[nodiscard]] virtual std::shared_ptr<Ui::DynamicImage> full(
|
||||
QSize size) const = 0;
|
||||
[[nodiscard]] virtual bool loaded() const = 0;
|
||||
[[nodiscard]] virtual bool loading() const = 0;
|
||||
[[nodiscard]] virtual double progress() const = 0;
|
||||
};
|
||||
|
||||
class ChannelRuntime {
|
||||
public:
|
||||
virtual ~ChannelRuntime() = default;
|
||||
|
||||
[[nodiscard]] virtual bool joinVisible() const = 0;
|
||||
virtual void open(Qt::MouseButton button) const = 0;
|
||||
virtual void join(Qt::MouseButton button) const = 0;
|
||||
};
|
||||
|
||||
class MediaRuntime {
|
||||
public:
|
||||
virtual ~MediaRuntime() = default;
|
||||
@@ -48,18 +87,43 @@ public:
|
||||
QSize size) const = 0;
|
||||
[[nodiscard]] virtual std::shared_ptr<PhotoRuntime> resolvePhoto(
|
||||
uint64 photoId) const = 0;
|
||||
[[nodiscard]] virtual std::shared_ptr<DocumentRuntime> resolveDocument(
|
||||
uint64 documentId) const {
|
||||
return nullptr;
|
||||
}
|
||||
[[nodiscard]] virtual std::shared_ptr<MapRuntime> resolveMap(
|
||||
double latitude,
|
||||
double longitude,
|
||||
uint64 accessHash,
|
||||
QSize size,
|
||||
int zoom) const {
|
||||
return nullptr;
|
||||
}
|
||||
[[nodiscard]] virtual std::shared_ptr<ChannelRuntime> resolveChannel(
|
||||
uint64 channelId,
|
||||
const QString &username) const {
|
||||
return nullptr;
|
||||
}
|
||||
[[nodiscard]] virtual rpl::producer<uint64> channelJoinedChanges() const {
|
||||
return rpl::never<uint64>();
|
||||
}
|
||||
};
|
||||
|
||||
enum class MediaActivationKind {
|
||||
None,
|
||||
ExternalUrl,
|
||||
Photo,
|
||||
Document,
|
||||
OpenChannel,
|
||||
JoinChannel,
|
||||
};
|
||||
|
||||
struct MediaActivation {
|
||||
MediaActivationKind kind = MediaActivationKind::None;
|
||||
QString url;
|
||||
std::shared_ptr<PhotoRuntime> photo;
|
||||
std::shared_ptr<DocumentRuntime> document;
|
||||
std::shared_ptr<ChannelRuntime> channel;
|
||||
};
|
||||
|
||||
enum class ViewerKind {
|
||||
|
||||
@@ -363,6 +363,12 @@ bool Controller::active() const {
|
||||
return _window && _window->isActiveWindow();
|
||||
}
|
||||
|
||||
void Controller::showJoinedTooltip() {
|
||||
if (_show) {
|
||||
_show->showToast(tr::lng_action_you_joined(tr::now));
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::minimize() {
|
||||
if (_window) {
|
||||
_window->setWindowState(_window->windowState() | Qt::WindowMinimized);
|
||||
|
||||
@@ -51,6 +51,7 @@ public:
|
||||
void updateOptions(OpenOptions options = {});
|
||||
|
||||
[[nodiscard]] bool active() const;
|
||||
void showJoinedTooltip();
|
||||
void minimize();
|
||||
|
||||
[[nodiscard]] rpl::producer<Event> events() const {
|
||||
|
||||
@@ -121,6 +121,12 @@ NativeInstantViewPrepareResult TryPrepareNativeInstantView(
|
||||
if (request.source->webpagePhoto) {
|
||||
RememberNativeIvPhoto(&state, *request.source->webpagePhoto);
|
||||
}
|
||||
for (const auto &document : request.source->page.data().vdocuments().v) {
|
||||
RememberNativeIvDocument(&state, document);
|
||||
}
|
||||
if (request.source->webpageDocument) {
|
||||
RememberNativeIvDocument(&state, *request.source->webpageDocument);
|
||||
}
|
||||
|
||||
if (!PrepareNativeIvBlocks(
|
||||
request.source->page.data().vblocks().v,
|
||||
|
||||
@@ -36,6 +36,11 @@ enum class PreparedBlockKind {
|
||||
Table,
|
||||
Details,
|
||||
Photo,
|
||||
Video,
|
||||
Audio,
|
||||
Map,
|
||||
Channel,
|
||||
GroupedMedia,
|
||||
Placeholder,
|
||||
};
|
||||
|
||||
@@ -103,6 +108,54 @@ struct PreparedPhotoBlockData {
|
||||
bool viewerOpen = false;
|
||||
};
|
||||
|
||||
enum class PreparedMediaItemKind {
|
||||
Photo,
|
||||
Document,
|
||||
};
|
||||
|
||||
struct PreparedMediaItemData {
|
||||
PreparedMediaItemKind kind = PreparedMediaItemKind::Photo;
|
||||
uint64 id = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
struct PreparedVideoBlockData {
|
||||
PreparedMediaItemData media;
|
||||
};
|
||||
|
||||
struct PreparedAudioBlockData {
|
||||
uint64 documentId = 0;
|
||||
QString title;
|
||||
QString performer;
|
||||
QString fileName;
|
||||
int duration = 0;
|
||||
};
|
||||
|
||||
struct PreparedMapBlockData {
|
||||
double latitude = 0.;
|
||||
double longitude = 0.;
|
||||
uint64 accessHash = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int zoom = 0;
|
||||
QString url;
|
||||
};
|
||||
|
||||
struct PreparedChannelBlockData {
|
||||
uint64 channelId = 0;
|
||||
QString title;
|
||||
QString username;
|
||||
};
|
||||
|
||||
struct PreparedGroupedMediaItemData {
|
||||
PreparedMediaItemData media;
|
||||
};
|
||||
|
||||
struct PreparedGroupedMediaBlockData {
|
||||
std::vector<PreparedGroupedMediaItemData> items;
|
||||
};
|
||||
|
||||
struct PreparedPlaceholderBlockData {
|
||||
QString label;
|
||||
QString copyText;
|
||||
@@ -119,6 +172,11 @@ struct PreparedBlock {
|
||||
QString formulaTex;
|
||||
QString anchorId;
|
||||
PreparedPhotoBlockData photo;
|
||||
PreparedVideoBlockData video;
|
||||
PreparedAudioBlockData audio;
|
||||
PreparedMapBlockData map;
|
||||
PreparedChannelBlockData channel;
|
||||
PreparedGroupedMediaBlockData groupedMedia;
|
||||
PreparedPlaceholderBlockData placeholder;
|
||||
ListKind listKind = ListKind::Bullet;
|
||||
ListDelimiter listDelimiter = ListDelimiter::None;
|
||||
|
||||
@@ -507,11 +507,7 @@ void SortPreparedIvRichText(PreparedIvRichText *text) {
|
||||
}, [&](const MTPDpageBlockPhoto &data) {
|
||||
return PrepareNativeIvPhotoBlock(data, result, state);
|
||||
}, [&](const MTPDpageBlockVideo &data) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
u"Video Placeholder"_q,
|
||||
data.vcaption(),
|
||||
result,
|
||||
state);
|
||||
return PrepareNativeIvVideoBlock(data, result, state);
|
||||
}, [&](const MTPDpageBlockCover &data) {
|
||||
return PrepareNativeIvBlock(data.vcover(), result, state);
|
||||
}, [&](const MTPDpageBlockEmbed &data) {
|
||||
@@ -527,27 +523,23 @@ void SortPreparedIvRichText(PreparedIvRichText *text) {
|
||||
result,
|
||||
state);
|
||||
}, [&](const MTPDpageBlockCollage &data) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
u"Collage placeholder"_q,
|
||||
return PrepareNativeIvGroupedMediaBlock(
|
||||
data.vitems().v,
|
||||
data.vcaption(),
|
||||
u"Collage placeholder"_q,
|
||||
result,
|
||||
state);
|
||||
}, [&](const MTPDpageBlockSlideshow &data) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
return PrepareNativeIvGroupedMediaBlock(
|
||||
data.vitems().v,
|
||||
data.vcaption(),
|
||||
u"Grouped Media Placeholder"_q,
|
||||
data.vcaption(),
|
||||
result,
|
||||
state);
|
||||
}, [&](const MTPDpageBlockChannel &) {
|
||||
return PrepareNativeIvPlainPlaceholderBlock(
|
||||
u"Channel Placeholder"_q,
|
||||
result);
|
||||
}, [&](const MTPDpageBlockChannel &data) {
|
||||
return PrepareNativeIvChannelBlock(data, result, state);
|
||||
}, [&](const MTPDpageBlockAudio &data) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
u"Audio File Placeholder"_q,
|
||||
data.vcaption(),
|
||||
result,
|
||||
state);
|
||||
return PrepareNativeIvAudioBlock(data, result, state);
|
||||
}, [&](const MTPDpageBlockKicker &data) {
|
||||
return AppendNativeIvFlowBlock(
|
||||
result,
|
||||
@@ -573,11 +565,7 @@ void SortPreparedIvRichText(PreparedIvRichText *text) {
|
||||
u"Related Articles Placeholder"_q,
|
||||
result);
|
||||
}, [&](const MTPDpageBlockMap &data) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
u"Map Placeholder"_q,
|
||||
data.vcaption(),
|
||||
result,
|
||||
state);
|
||||
return PrepareNativeIvMapBlock(data, result, state);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,13 @@ For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "iv/markdown/iv_markdown_prepare_native_richtext.h"
|
||||
|
||||
struct GeoPointLocation;
|
||||
|
||||
#include "data/data_location.h"
|
||||
#include "iv/markdown/iv_markdown_prepare_links.h"
|
||||
#include "ui/basic_click_handlers.h"
|
||||
#include "history/history_location_manager.h"
|
||||
|
||||
#include <QtCore/QUrl>
|
||||
|
||||
@@ -41,6 +46,78 @@ void ShiftEntities(EntitiesInText *entities, int delta) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] const NativeIvDocumentInfo *FindNativeIvDocument(
|
||||
uint64 documentId,
|
||||
const NativeIvPrepareState &state) {
|
||||
for (const auto &document : state.documents) {
|
||||
if (document.id == documentId) {
|
||||
return &document;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void MergeNativeIvDocumentInfo(
|
||||
NativeIvDocumentInfo *existing,
|
||||
NativeIvDocumentInfo info) {
|
||||
if (!existing || (existing->id != info.id)) {
|
||||
return;
|
||||
}
|
||||
if (existing->width <= 0 && info.width > 0) {
|
||||
existing->width = info.width;
|
||||
}
|
||||
if (existing->height <= 0 && info.height > 0) {
|
||||
existing->height = info.height;
|
||||
}
|
||||
if (existing->fileName.isEmpty() && !info.fileName.isEmpty()) {
|
||||
existing->fileName = std::move(info.fileName);
|
||||
}
|
||||
if (existing->title.isEmpty() && !info.title.isEmpty()) {
|
||||
existing->title = std::move(info.title);
|
||||
}
|
||||
if (existing->performer.isEmpty() && !info.performer.isEmpty()) {
|
||||
existing->performer = std::move(info.performer);
|
||||
}
|
||||
if (existing->duration <= 0 && info.duration > 0) {
|
||||
existing->duration = info.duration;
|
||||
}
|
||||
if (!existing->isVideoFile && info.isVideoFile) {
|
||||
existing->isVideoFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool PrepareNativeIvGroupedMediaItem(
|
||||
const MTPPageBlock &item,
|
||||
PreparedGroupedMediaItemData *result,
|
||||
const NativeIvPrepareState &state) {
|
||||
return item.match([&](const MTPDpageBlockPhoto &data) {
|
||||
const auto info = FindNativeIvPhoto(uint64(data.vphoto_id().v), state);
|
||||
if (!info || info->width <= 0 || info->height <= 0) {
|
||||
return false;
|
||||
}
|
||||
result->media.kind = PreparedMediaItemKind::Photo;
|
||||
result->media.id = info->id;
|
||||
result->media.width = info->width;
|
||||
result->media.height = info->height;
|
||||
return true;
|
||||
}, [&](const MTPDpageBlockVideo &data) {
|
||||
const auto info = FindNativeIvDocument(uint64(data.vvideo_id().v), state);
|
||||
if (!info
|
||||
|| !info->isVideoFile
|
||||
|| info->width <= 0
|
||||
|| info->height <= 0) {
|
||||
return false;
|
||||
}
|
||||
result->media.kind = PreparedMediaItemKind::Document;
|
||||
result->media.id = info->id;
|
||||
result->media.width = info->width;
|
||||
result->media.height = info->height;
|
||||
return true;
|
||||
}, [](const auto &) {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void SortPreparedIvRichText(PreparedIvRichText *text) {
|
||||
SortEntities(&text->text);
|
||||
}
|
||||
@@ -430,6 +507,51 @@ void RememberNativeIvPhoto(
|
||||
state->photos.push_back(info);
|
||||
}
|
||||
|
||||
void RememberNativeIvDocument(
|
||||
NativeIvPrepareState *state,
|
||||
const MTPDocument &document) {
|
||||
auto info = NativeIvDocumentInfo{
|
||||
.id = document.match([](const auto &data) {
|
||||
return data.vid().v;
|
||||
}),
|
||||
};
|
||||
document.match([](const MTPDdocumentEmpty &) {
|
||||
}, [&](const MTPDdocument &data) {
|
||||
const auto assignDimensions = [&](int width, int height, bool force) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
if (force || info.width <= 0 || info.height <= 0) {
|
||||
info.width = width;
|
||||
info.height = height;
|
||||
}
|
||||
};
|
||||
for (const auto &attribute : data.vattributes().v) {
|
||||
attribute.match([&](const MTPDdocumentAttributeAudio &data) {
|
||||
info.duration = data.vduration().v;
|
||||
info.title = qs(data.vtitle().value_or_empty());
|
||||
info.performer = qs(data.vperformer().value_or_empty());
|
||||
}, [&](const MTPDdocumentAttributeFilename &data) {
|
||||
info.fileName = qs(data.vfile_name());
|
||||
}, [&](const MTPDdocumentAttributeImageSize &data) {
|
||||
assignDimensions(data.vw().v, data.vh().v, false);
|
||||
}, [&](const MTPDdocumentAttributeVideo &data) {
|
||||
info.isVideoFile = true;
|
||||
assignDimensions(data.vw().v, data.vh().v, true);
|
||||
}, [&](const auto &) {});
|
||||
}
|
||||
});
|
||||
if (!info.id) {
|
||||
return;
|
||||
}
|
||||
if (const auto existing = FindNativeIvDocument(info.id, *state)) {
|
||||
const auto index = existing - state->documents.data();
|
||||
MergeNativeIvDocumentInfo(&state->documents[index], std::move(info));
|
||||
return;
|
||||
}
|
||||
state->documents.push_back(std::move(info));
|
||||
}
|
||||
|
||||
bool PrepareNativeIvRichText(
|
||||
const MTPRichText &text,
|
||||
PreparedIvRichText *result,
|
||||
@@ -507,6 +629,201 @@ bool PrepareNativeIvPhotoBlock(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrepareNativeIvVideoBlock(
|
||||
const MTPDpageBlockVideo &data,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *state) {
|
||||
const auto info = FindNativeIvDocument(uint64(data.vvideo_id().v), *state);
|
||||
if (!info
|
||||
|| !info->isVideoFile
|
||||
|| info->width <= 0
|
||||
|| info->height <= 0) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
u"Video Placeholder"_q,
|
||||
data.vcaption(),
|
||||
result,
|
||||
state);
|
||||
}
|
||||
auto caption = PreparedIvRichText();
|
||||
auto anchorId = QString();
|
||||
if (!PrepareNativeIvCaption(data.vcaption(), &caption, &anchorId, state)) {
|
||||
return false;
|
||||
}
|
||||
SortPreparedIvRichText(&caption);
|
||||
auto block = PreparedBlock();
|
||||
block.kind = PreparedBlockKind::Video;
|
||||
block.text = std::move(caption.text);
|
||||
block.links = std::move(caption.links);
|
||||
block.anchorId = std::move(anchorId);
|
||||
block.video.media.kind = PreparedMediaItemKind::Document;
|
||||
block.video.media.id = info->id;
|
||||
block.video.media.width = info->width;
|
||||
block.video.media.height = info->height;
|
||||
result->push_back(std::move(block));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrepareNativeIvAudioBlock(
|
||||
const MTPDpageBlockAudio &data,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *state) {
|
||||
const auto info = FindNativeIvDocument(uint64(data.vaudio_id().v), *state);
|
||||
if (!info) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
u"Audio File Placeholder"_q,
|
||||
data.vcaption(),
|
||||
result,
|
||||
state);
|
||||
}
|
||||
auto caption = PreparedIvRichText();
|
||||
auto anchorId = QString();
|
||||
if (!PrepareNativeIvCaption(data.vcaption(), &caption, &anchorId, state)) {
|
||||
return false;
|
||||
}
|
||||
SortPreparedIvRichText(&caption);
|
||||
auto block = PreparedBlock();
|
||||
block.kind = PreparedBlockKind::Audio;
|
||||
block.text = std::move(caption.text);
|
||||
block.links = std::move(caption.links);
|
||||
block.anchorId = std::move(anchorId);
|
||||
block.audio.documentId = info->id;
|
||||
block.audio.title = info->title;
|
||||
block.audio.performer = info->performer;
|
||||
block.audio.fileName = info->fileName;
|
||||
block.audio.duration = info->duration;
|
||||
result->push_back(std::move(block));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrepareNativeIvMapBlock(
|
||||
const MTPDpageBlockMap &data,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *state) {
|
||||
auto prepared = PreparedMapBlockData();
|
||||
const auto supported = data.vgeo().match([&](const MTPDgeoPoint &geo) {
|
||||
if (!geo.vaccess_hash().v || data.vw().v <= 0 || data.vh().v <= 0) {
|
||||
return false;
|
||||
}
|
||||
prepared.latitude = geo.vlat().v;
|
||||
prepared.longitude = geo.vlong().v;
|
||||
prepared.accessHash = geo.vaccess_hash().v;
|
||||
prepared.width = data.vw().v;
|
||||
prepared.height = data.vh().v;
|
||||
prepared.zoom = data.vzoom().v;
|
||||
prepared.url = LocationClickHandler::Url(Data::LocationPoint(geo));
|
||||
return true;
|
||||
}, [](const auto &) {
|
||||
return false;
|
||||
});
|
||||
if (!supported) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
u"Map Placeholder"_q,
|
||||
data.vcaption(),
|
||||
result,
|
||||
state);
|
||||
}
|
||||
auto caption = PreparedIvRichText();
|
||||
auto anchorId = QString();
|
||||
if (!PrepareNativeIvCaption(data.vcaption(), &caption, &anchorId, state)) {
|
||||
return false;
|
||||
}
|
||||
SortPreparedIvRichText(&caption);
|
||||
auto block = PreparedBlock();
|
||||
block.kind = PreparedBlockKind::Map;
|
||||
block.text = std::move(caption.text);
|
||||
block.links = std::move(caption.links);
|
||||
block.anchorId = std::move(anchorId);
|
||||
block.map = std::move(prepared);
|
||||
result->push_back(std::move(block));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrepareNativeIvChannelBlock(
|
||||
const MTPDpageBlockChannel &data,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *) {
|
||||
auto prepared = PreparedChannelBlockData();
|
||||
const auto supported = data.vchannel().match([&](const MTPDchannel &channel) {
|
||||
prepared.channelId = channel.vid().v;
|
||||
prepared.title = qs(channel.vtitle());
|
||||
prepared.username = qs(channel.vusername().value_or_empty());
|
||||
return true;
|
||||
}, [&](const MTPDchannelForbidden &channel) {
|
||||
prepared.channelId = channel.vid().v;
|
||||
prepared.title = qs(channel.vtitle());
|
||||
return true;
|
||||
}, [&](const MTPDchat &channel) {
|
||||
prepared.channelId = channel.vid().v;
|
||||
prepared.title = qs(channel.vtitle());
|
||||
return true;
|
||||
}, [&](const MTPDchatForbidden &channel) {
|
||||
prepared.channelId = channel.vid().v;
|
||||
prepared.title = qs(channel.vtitle());
|
||||
return true;
|
||||
}, [](const auto &) {
|
||||
return false;
|
||||
});
|
||||
if (!supported || !prepared.channelId || prepared.title.isEmpty()) {
|
||||
return PrepareNativeIvPlainPlaceholderBlock(
|
||||
u"Channel Placeholder"_q,
|
||||
result);
|
||||
}
|
||||
auto block = PreparedBlock();
|
||||
block.kind = PreparedBlockKind::Channel;
|
||||
block.channel = std::move(prepared);
|
||||
result->push_back(std::move(block));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrepareNativeIvGroupedMediaBlock(
|
||||
const QVector<MTPPageBlock> &items,
|
||||
const MTPPageCaption &caption,
|
||||
QString placeholderLabel,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *state) {
|
||||
if (items.size() < 2) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
std::move(placeholderLabel),
|
||||
caption,
|
||||
result,
|
||||
state);
|
||||
}
|
||||
auto block = PreparedBlock();
|
||||
block.kind = PreparedBlockKind::GroupedMedia;
|
||||
block.groupedMedia.items.reserve(items.size());
|
||||
for (const auto &item : items) {
|
||||
auto prepared = PreparedGroupedMediaItemData();
|
||||
if (!PrepareNativeIvGroupedMediaItem(item, &prepared, *state)) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
std::move(placeholderLabel),
|
||||
caption,
|
||||
result,
|
||||
state);
|
||||
}
|
||||
block.groupedMedia.items.push_back(std::move(prepared));
|
||||
}
|
||||
if (block.groupedMedia.items.empty()) {
|
||||
return PrepareNativeIvPlaceholderBlock(
|
||||
std::move(placeholderLabel),
|
||||
caption,
|
||||
result,
|
||||
state);
|
||||
}
|
||||
auto preparedCaption = PreparedIvRichText();
|
||||
if (!PrepareNativeIvCaption(
|
||||
caption,
|
||||
&preparedCaption,
|
||||
&block.anchorId,
|
||||
state)) {
|
||||
return false;
|
||||
}
|
||||
SortPreparedIvRichText(&preparedCaption);
|
||||
block.text = std::move(preparedCaption.text);
|
||||
block.links = std::move(preparedCaption.links);
|
||||
result->push_back(std::move(block));
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] QString NativeIvPlaceholderCopyText(
|
||||
|
||||
@@ -19,6 +19,9 @@ struct PreparedIvRichText {
|
||||
void RememberNativeIvPhoto(
|
||||
NativeIvPrepareState *state,
|
||||
const MTPPhoto &photo);
|
||||
void RememberNativeIvDocument(
|
||||
NativeIvPrepareState *state,
|
||||
const MTPDocument &document);
|
||||
[[nodiscard]] bool PrepareNativeIvPlainPlaceholderBlock(
|
||||
QString label,
|
||||
std::vector<PreparedBlock> *result);
|
||||
@@ -31,6 +34,28 @@ void RememberNativeIvPhoto(
|
||||
const MTPDpageBlockPhoto &data,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *state);
|
||||
[[nodiscard]] bool PrepareNativeIvVideoBlock(
|
||||
const MTPDpageBlockVideo &data,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *state);
|
||||
[[nodiscard]] bool PrepareNativeIvAudioBlock(
|
||||
const MTPDpageBlockAudio &data,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *state);
|
||||
[[nodiscard]] bool PrepareNativeIvMapBlock(
|
||||
const MTPDpageBlockMap &data,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *state);
|
||||
[[nodiscard]] bool PrepareNativeIvChannelBlock(
|
||||
const MTPDpageBlockChannel &data,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *state);
|
||||
[[nodiscard]] bool PrepareNativeIvGroupedMediaBlock(
|
||||
const QVector<MTPPageBlock> &items,
|
||||
const MTPPageCaption &caption,
|
||||
QString placeholderLabel,
|
||||
std::vector<PreparedBlock> *result,
|
||||
NativeIvPrepareState *state);
|
||||
[[nodiscard]] bool PrepareNativeIvRichText(
|
||||
const MTPRichText &text,
|
||||
PreparedIvRichText *result,
|
||||
|
||||
@@ -58,9 +58,21 @@ struct NativeIvPhotoInfo {
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
struct NativeIvDocumentInfo {
|
||||
uint64 id = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
QString fileName;
|
||||
QString title;
|
||||
QString performer;
|
||||
int duration = 0;
|
||||
bool isVideoFile = false;
|
||||
};
|
||||
|
||||
struct NativeIvPrepareState {
|
||||
MarkdownArticleContent result;
|
||||
std::vector<NativeIvPhotoInfo> photos;
|
||||
std::vector<NativeIvDocumentInfo> documents;
|
||||
int nextGeneratedId = 0;
|
||||
|
||||
void setFailure(
|
||||
|
||||
@@ -198,8 +198,10 @@ private:
|
||||
Ui::FlatLabel *_failure = nullptr;
|
||||
Ui::LinkButton *_failureOpen = nullptr;
|
||||
const std::shared_ptr<MathRenderer> _renderer;
|
||||
std::shared_ptr<MarkdownArticle> _article;
|
||||
QString _pendingFragment;
|
||||
int _devicePixelRatio = 0;
|
||||
rpl::lifetime _channelJoinedLifetime;
|
||||
|
||||
};
|
||||
|
||||
@@ -451,7 +453,9 @@ void MarkdownPreviewRoot::applyPreparedContent(
|
||||
int prepareMs) {
|
||||
const auto failure = prepared.failure;
|
||||
const auto debug = prepared.debug;
|
||||
_channelJoinedLifetime.destroy();
|
||||
if (failure.failed()) {
|
||||
_article = nullptr;
|
||||
_footnotes.clear();
|
||||
_scroll->hide();
|
||||
if (_body) {
|
||||
@@ -473,14 +477,27 @@ void MarkdownPreviewRoot::applyPreparedContent(
|
||||
_footnotes = prepared.footnotes;
|
||||
|
||||
if (!_body) {
|
||||
_article = nullptr;
|
||||
logPreparationSummary(failure, debug, prepareMs, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prepared.mediaRuntime) {
|
||||
prepared.mediaRuntime->channelJoinedChanges(
|
||||
) | rpl::on_next([=](uint64) {
|
||||
if (_body && !_body->isHidden() && _article) {
|
||||
_article->invalidateLayout();
|
||||
updateChildrenGeometry(size());
|
||||
_body->update();
|
||||
}
|
||||
}, _channelJoinedLifetime);
|
||||
}
|
||||
|
||||
auto article = std::make_shared<MarkdownArticle>(_renderer);
|
||||
article->setContent(std::move(prepared));
|
||||
_article = article;
|
||||
updateChildrenGeometry(size());
|
||||
_body->setArticle(std::move(article));
|
||||
_body->setArticle(article);
|
||||
if (_options.delegate) {
|
||||
_body->setZoom(_options.delegate->ivZoom());
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user