init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_abstract_single_file_preview.h"
|
||||
|
||||
#include "base/timer_rpl.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
AbstractSingleFilePreview::AbstractSingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
AttachControls::Type type)
|
||||
: AbstractSinglePreview(parent)
|
||||
, _st(st)
|
||||
, _type(type)
|
||||
, _editMedia(this, _st.files.buttonFile)
|
||||
, _deleteMedia(this, _st.files.buttonFile) {
|
||||
|
||||
_editMedia->setIconOverride(&_st.files.buttonFileEdit);
|
||||
_deleteMedia->setIconOverride(&_st.files.buttonFileDelete);
|
||||
|
||||
if (type == AttachControls::Type::Full) {
|
||||
_deleteMedia->show();
|
||||
_editMedia->show();
|
||||
} else if (type == AttachControls::Type::EditOnly) {
|
||||
_deleteMedia->hide();
|
||||
_editMedia->show();
|
||||
} else if (type == AttachControls::Type::None) {
|
||||
_deleteMedia->hide();
|
||||
_editMedia->hide();
|
||||
}
|
||||
}
|
||||
|
||||
AbstractSingleFilePreview::~AbstractSingleFilePreview() = default;
|
||||
|
||||
rpl::producer<> AbstractSingleFilePreview::editRequests() const {
|
||||
return _editMedia->clicks() | rpl::map([] {
|
||||
return base::timer_once(st::historyAttach.ripple.hideDuration);
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleFilePreview::deleteRequests() const {
|
||||
return _deleteMedia->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleFilePreview::modifyRequests() const {
|
||||
return rpl::never<>();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleFilePreview::editCoverRequests() const {
|
||||
return rpl::never<>();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleFilePreview::clearCoverRequests() const {
|
||||
return rpl::never<>();
|
||||
}
|
||||
|
||||
void AbstractSingleFilePreview::prepareThumbFor(
|
||||
Data &data,
|
||||
const QImage &preview) {
|
||||
if (preview.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto originalWidth = preview.width();
|
||||
auto originalHeight = preview.height();
|
||||
const auto &st = st::attachPreviewThumbLayout;
|
||||
auto thumbWidth = st.thumbSize;
|
||||
if (originalWidth > originalHeight) {
|
||||
thumbWidth = (originalWidth * st.thumbSize) / originalHeight;
|
||||
}
|
||||
const auto options = Images::Option::RoundSmall;
|
||||
data.fileThumb = PixmapFromImage(Images::Prepare(
|
||||
preview,
|
||||
thumbWidth * style::DevicePixelRatio(),
|
||||
{ .options = options, .outer = { st.thumbSize, st.thumbSize } }));
|
||||
}
|
||||
|
||||
void AbstractSingleFilePreview::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
const auto w = width()
|
||||
- st::boxPhotoPadding.left()
|
||||
- st::boxPhotoPadding.right();
|
||||
const auto &st = !isThumbedLayout(_data)
|
||||
? st::attachPreviewLayout
|
||||
: st::attachPreviewThumbLayout;
|
||||
const auto nameleft = st.thumbSize + st.thumbSkip;
|
||||
const auto nametop = st.nameTop;
|
||||
const auto statustop = st.statusTop;
|
||||
const auto x = (width() - w) / 2, y = 0;
|
||||
|
||||
if (!isThumbedLayout(_data)) {
|
||||
QRect inner(
|
||||
style::rtlrect(x, y, st.thumbSize, st.thumbSize, width()));
|
||||
p.setPen(Qt::NoPen);
|
||||
|
||||
if (_data.fileIsAudio && !_data.fileThumb.isNull()) {
|
||||
p.drawPixmap(inner.topLeft(), _data.fileThumb);
|
||||
} else {
|
||||
p.setBrush(_st.files.iconBg);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawEllipse(inner);
|
||||
}
|
||||
auto &icon = _data.fileIsAudio
|
||||
? (_data.fileThumb.isNull()
|
||||
? _st.files.iconPlay
|
||||
: st::historyFileThumbPlay)
|
||||
: _data.fileIsImage
|
||||
? _st.files.iconImage
|
||||
: _st.files.iconDocument;
|
||||
icon.paintInCenter(p, inner);
|
||||
} else {
|
||||
QRect rthumb(
|
||||
style::rtlrect(x, y, st.thumbSize, st.thumbSize, width()));
|
||||
p.drawPixmap(rthumb.topLeft(), _data.fileThumb);
|
||||
}
|
||||
p.setFont(st::semiboldFont);
|
||||
p.setPen(_st.files.nameFg);
|
||||
p.drawTextLeft(
|
||||
x + nameleft,
|
||||
y + nametop, width(),
|
||||
_data.name,
|
||||
_data.nameWidth);
|
||||
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(_st.files.statusFg);
|
||||
p.drawTextLeft(
|
||||
x + nameleft,
|
||||
y + statustop,
|
||||
width(),
|
||||
_data.statusText,
|
||||
_data.statusWidth);
|
||||
}
|
||||
|
||||
void AbstractSingleFilePreview::resizeEvent(QResizeEvent *e) {
|
||||
const auto w = width()
|
||||
- st::boxPhotoPadding.left()
|
||||
- st::boxPhotoPadding.right();
|
||||
const auto x = (width() - w) / 2;
|
||||
const auto top = st::sendBoxFileGroupSkipTop;
|
||||
auto right = st::sendBoxFileGroupSkipRight + x;
|
||||
if (_type != AttachControls::Type::EditOnly) {
|
||||
_deleteMedia->moveToRight(right, top);
|
||||
right += st::sendBoxFileGroupEditInternalSkip + _deleteMedia->width();
|
||||
}
|
||||
_editMedia->moveToRight(right, top);
|
||||
}
|
||||
|
||||
bool AbstractSingleFilePreview::isThumbedLayout(Data &data) const {
|
||||
return (!data.fileThumb.isNull() && !data.fileIsAudio);
|
||||
}
|
||||
|
||||
void AbstractSingleFilePreview::updateTextWidthFor(Data &data) {
|
||||
const auto &st = !isThumbedLayout(data)
|
||||
? st::attachPreviewLayout
|
||||
: st::attachPreviewThumbLayout;
|
||||
const auto buttonsCount = (_type == AttachControls::Type::EditOnly)
|
||||
? 1
|
||||
: (_type == AttachControls::Type::Full)
|
||||
? 2
|
||||
: 0;
|
||||
const auto availableFileWidth = st::sendMediaPreviewSize
|
||||
- st.thumbSize
|
||||
- st.thumbSkip
|
||||
// Right buttons.
|
||||
- _st.files.buttonFile.width * buttonsCount
|
||||
- st::sendBoxAlbumGroupEditInternalSkip * buttonsCount
|
||||
- st::sendBoxAlbumGroupSkipRight;
|
||||
data.nameWidth = st::semiboldFont->width(data.name);
|
||||
if (data.nameWidth > availableFileWidth) {
|
||||
data.name = st::semiboldFont->elided(
|
||||
data.name,
|
||||
availableFileWidth,
|
||||
Qt::ElideMiddle);
|
||||
data.nameWidth = st::semiboldFont->width(data.name);
|
||||
}
|
||||
data.statusWidth = st::normalFont->width(data.statusText);
|
||||
}
|
||||
|
||||
void AbstractSingleFilePreview::setData(const Data &data) {
|
||||
_data = data;
|
||||
|
||||
updateTextWidthFor(_data);
|
||||
|
||||
const auto &st = !isThumbedLayout(_data)
|
||||
? st::attachPreviewLayout
|
||||
: st::attachPreviewThumbLayout;
|
||||
resize(width(), st.thumbSize);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/chat/attach/attach_abstract_single_preview.h"
|
||||
#include "ui/chat/attach/attach_controls.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace style {
|
||||
struct ComposeControls;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class IconButton;
|
||||
|
||||
class AbstractSingleFilePreview : public AbstractSinglePreview {
|
||||
public:
|
||||
AbstractSingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
AttachControls::Type type);
|
||||
~AbstractSingleFilePreview();
|
||||
|
||||
[[nodiscard]] rpl::producer<> deleteRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> editRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> modifyRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> editCoverRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> clearCoverRequests() const override;
|
||||
|
||||
protected:
|
||||
struct Data {
|
||||
QPixmap fileThumb;
|
||||
QString name;
|
||||
QString statusText;
|
||||
int nameWidth = 0;
|
||||
int statusWidth = 0;
|
||||
bool fileIsAudio = false;
|
||||
bool fileIsImage = false;
|
||||
};
|
||||
|
||||
void prepareThumbFor(Data &data, const QImage &preview);
|
||||
bool isThumbedLayout(Data &data) const;
|
||||
|
||||
void setData(const Data &data);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
void updateTextWidthFor(Data &data);
|
||||
|
||||
const style::ComposeControls &_st;
|
||||
const AttachControls::Type _type;
|
||||
|
||||
Data _data;
|
||||
|
||||
object_ptr<IconButton> _editMedia = { nullptr };
|
||||
object_ptr<IconButton> _deleteMedia = { nullptr };
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_abstract_single_media_preview.h"
|
||||
|
||||
#include "editor/photo_editor_common.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/chat/attach/attach_controls.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMinPreviewWidth = 20;
|
||||
|
||||
} // namespace
|
||||
|
||||
AbstractSingleMediaPreview::AbstractSingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
AttachControls::Type type,
|
||||
Fn<bool(AttachActionType)> actionAllowed)
|
||||
: AbstractSinglePreview(parent)
|
||||
, _st(st)
|
||||
, _actionAllowed(std::move(actionAllowed))
|
||||
, _minThumbH(st::sendBoxAlbumGroupSize.height()
|
||||
+ st::sendBoxAlbumGroupSkipTop * 2)
|
||||
, _controls(base::make_unique_q<AttachControlsWidget>(this, type)) {
|
||||
}
|
||||
|
||||
AbstractSingleMediaPreview::~AbstractSingleMediaPreview() = default;
|
||||
|
||||
rpl::producer<> AbstractSingleMediaPreview::deleteRequests() const {
|
||||
return _controls->deleteRequests();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleMediaPreview::editRequests() const {
|
||||
return _controls->editRequests();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleMediaPreview::modifyRequests() const {
|
||||
return _photoEditorRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleMediaPreview::editCoverRequests() const {
|
||||
return _editCoverRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleMediaPreview::clearCoverRequests() const {
|
||||
return _clearCoverRequests.events();
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::setSendWay(SendFilesWay way) {
|
||||
_sendWay = way;
|
||||
update();
|
||||
}
|
||||
|
||||
SendFilesWay AbstractSingleMediaPreview::sendWay() const {
|
||||
return _sendWay;
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::setSpoiler(bool spoiler) {
|
||||
_spoiler = spoiler
|
||||
? std::make_unique<SpoilerAnimation>([=] { update(); })
|
||||
: nullptr;
|
||||
update();
|
||||
}
|
||||
|
||||
bool AbstractSingleMediaPreview::hasSpoiler() const {
|
||||
return _spoiler != nullptr;
|
||||
}
|
||||
|
||||
bool AbstractSingleMediaPreview::canHaveSpoiler() const {
|
||||
return supportsSpoilers();
|
||||
}
|
||||
|
||||
rpl::producer<bool> AbstractSingleMediaPreview::spoileredChanges() const {
|
||||
return _spoileredChanges.events();
|
||||
}
|
||||
|
||||
QImage AbstractSingleMediaPreview::generatePriceTagBackground() const {
|
||||
return (_previewBlurred.isNull() ? _preview : _previewBlurred).toImage();
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::preparePreview(QImage preview) {
|
||||
auto maxW = 0;
|
||||
auto maxH = 0;
|
||||
if (_animated && drawBackground()) {
|
||||
auto limitW = st::sendMediaPreviewSize;
|
||||
auto limitH = st::confirmMaxHeight;
|
||||
maxW = qMax(preview.width(), 1);
|
||||
maxH = qMax(preview.height(), 1);
|
||||
if (maxW * limitH > maxH * limitW) {
|
||||
if (maxW < limitW) {
|
||||
maxH = maxH * limitW / maxW;
|
||||
maxW = limitW;
|
||||
}
|
||||
} else {
|
||||
if (maxH < limitH) {
|
||||
maxW = maxW * limitH / maxH;
|
||||
maxH = limitH;
|
||||
}
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
preview = Images::Prepare(
|
||||
std::move(preview),
|
||||
QSize(maxW, maxH) * ratio,
|
||||
{ .outer = { maxW, maxH } });
|
||||
}
|
||||
auto originalWidth = preview.width();
|
||||
auto originalHeight = preview.height();
|
||||
if (!originalWidth || !originalHeight) {
|
||||
originalWidth = originalHeight = 1;
|
||||
}
|
||||
_previewWidth = st::sendMediaPreviewSize;
|
||||
if (preview.width() < _previewWidth) {
|
||||
_previewWidth = qMax(preview.width(), kMinPreviewWidth);
|
||||
}
|
||||
auto maxthumbh = qMin(qRound(1.5 * _previewWidth), st::confirmMaxHeight);
|
||||
_previewHeight = qRound(originalHeight
|
||||
* float64(_previewWidth)
|
||||
/ originalWidth);
|
||||
if (_previewHeight > maxthumbh) {
|
||||
_previewWidth = qRound(_previewWidth
|
||||
* float64(maxthumbh)
|
||||
/ _previewHeight);
|
||||
accumulate_max(_previewWidth, kMinPreviewWidth);
|
||||
_previewHeight = maxthumbh;
|
||||
}
|
||||
_previewLeft = (st::boxWideWidth - _previewWidth) / 2;
|
||||
if (_previewHeight < _minThumbH) {
|
||||
_previewTop = (_minThumbH - _previewHeight) / 2;
|
||||
}
|
||||
|
||||
preview = std::move(preview).scaled(
|
||||
_previewWidth * style::DevicePixelRatio(),
|
||||
_previewHeight * style::DevicePixelRatio(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
preview = Images::Opaque(std::move(preview));
|
||||
_preview = PixmapFromImage(std::move(preview));
|
||||
_preview.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
_previewBlurred = QPixmap();
|
||||
|
||||
resize(width(), std::max(_previewHeight, _minThumbH));
|
||||
}
|
||||
|
||||
bool AbstractSingleMediaPreview::isOverPreview(QPoint position) const {
|
||||
return QRect(
|
||||
_previewLeft,
|
||||
_previewTop,
|
||||
_previewWidth,
|
||||
_previewHeight).contains(position);
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::resizeEvent(QResizeEvent *e) {
|
||||
_controls->moveToRight(
|
||||
st::boxPhotoPadding.right() + st::sendBoxAlbumGroupSkipRight,
|
||||
st::sendBoxAlbumGroupSkipTop,
|
||||
width());
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
auto takenSpoiler = supportsSpoilers()
|
||||
? nullptr
|
||||
: base::take(_spoiler);
|
||||
const auto guard = gsl::finally([&] {
|
||||
if (takenSpoiler) {
|
||||
_spoiler = base::take(takenSpoiler);
|
||||
}
|
||||
});
|
||||
|
||||
if (drawBackground()) {
|
||||
const auto &padding = st::boxPhotoPadding;
|
||||
if (_previewLeft > padding.left()) {
|
||||
p.fillRect(
|
||||
padding.left(),
|
||||
_previewTop,
|
||||
_previewLeft - padding.left(),
|
||||
_previewHeight,
|
||||
_st.files.confirmBg);
|
||||
}
|
||||
if ((_previewLeft + _previewWidth) < (width() - padding.right())) {
|
||||
p.fillRect(
|
||||
_previewLeft + _previewWidth,
|
||||
_previewTop,
|
||||
width() - padding.right() - _previewLeft - _previewWidth,
|
||||
_previewHeight,
|
||||
_st.files.confirmBg);
|
||||
}
|
||||
if (_previewTop > 0) {
|
||||
p.fillRect(
|
||||
padding.left(),
|
||||
0,
|
||||
width() - padding.right() - padding.left(),
|
||||
height(),
|
||||
_st.files.confirmBg);
|
||||
}
|
||||
}
|
||||
|
||||
if (_spoiler && _previewBlurred.isNull()) {
|
||||
_previewBlurred = BlurredPreviewFromPixmap(_preview, RectPart::None);
|
||||
}
|
||||
if (_spoiler || !tryPaintAnimation(p)) {
|
||||
const auto &pixmap = _spoiler ? _previewBlurred : _preview;
|
||||
const auto position = QPoint(_previewLeft, _previewTop);
|
||||
p.drawPixmap(position, pixmap);
|
||||
if (_spoiler) {
|
||||
const auto paused = On(PowerSaving::kChatSpoiler);
|
||||
FillSpoilerRect(
|
||||
p,
|
||||
QRect(position, pixmap.size() / pixmap.devicePixelRatio()),
|
||||
DefaultImageSpoiler().frame(
|
||||
_spoiler->index(crl::now(), paused)));
|
||||
}
|
||||
}
|
||||
if (_animated && !isAnimatedPreviewReady() && !_spoiler) {
|
||||
const auto innerSize = st::msgFileLayout.thumbSize;
|
||||
auto inner = QRect(
|
||||
_previewLeft + (_previewWidth - innerSize) / 2,
|
||||
_previewTop + (_previewHeight - innerSize) / 2,
|
||||
innerSize,
|
||||
innerSize);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::msgDateImgBg);
|
||||
|
||||
{
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawEllipse(inner);
|
||||
}
|
||||
|
||||
auto icon = &st::historyFileInPlay;
|
||||
icon->paintInCenter(p, inner);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::mousePressEvent(QMouseEvent *e) {
|
||||
if (isOverPreview(e->pos())) {
|
||||
_pressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::mouseMoveEvent(QMouseEvent *e) {
|
||||
applyCursor((isPhoto() && isOverPreview(e->pos()))
|
||||
? style::cur_pointer
|
||||
: style::cur_default);
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::mouseReleaseEvent(QMouseEvent *e) {
|
||||
if (base::take(_pressed) && isOverPreview(e->pos())) {
|
||||
if (e->button() == Qt::RightButton) {
|
||||
showContextMenu(e->globalPos());
|
||||
} else if (isPhoto()) {
|
||||
_photoEditorRequests.fire({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::applyCursor(style::cursor cursor) {
|
||||
if (_cursor != cursor) {
|
||||
_cursor = cursor;
|
||||
setCursor(_cursor);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::showContextMenu(QPoint position) {
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
_st.tabbed.menu);
|
||||
|
||||
const auto &icons = _st.tabbed.icons;
|
||||
if (_actionAllowed(AttachActionType::ToggleSpoiler)
|
||||
&& _sendWay.sendImagesAsPhotos()
|
||||
&& supportsSpoilers()) {
|
||||
const auto spoilered = hasSpoiler();
|
||||
_menu->addAction(spoilered
|
||||
? tr::lng_context_disable_spoiler(tr::now)
|
||||
: tr::lng_context_spoiler_effect(tr::now), [=] {
|
||||
setSpoiler(!spoilered);
|
||||
_spoileredChanges.fire_copy(!spoilered);
|
||||
}, spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler);
|
||||
}
|
||||
if (_actionAllowed(AttachActionType::EditCover)) {
|
||||
_menu->addAction(tr::lng_context_edit_cover(tr::now), [=] {
|
||||
_editCoverRequests.fire({});
|
||||
}, &st::menuIconEdit);
|
||||
|
||||
if (_actionAllowed(AttachActionType::ClearCover)) {
|
||||
_menu->addAction(tr::lng_context_clear_cover(tr::now), [=] {
|
||||
_clearCoverRequests.fire({});
|
||||
}, &st::menuIconCancel);
|
||||
}
|
||||
}
|
||||
if (_menu->empty()) {
|
||||
_menu = nullptr;
|
||||
} else {
|
||||
_menu->popup(position);
|
||||
}
|
||||
}
|
||||
|
||||
int AbstractSingleMediaPreview::previewLeft() const {
|
||||
return _previewLeft;
|
||||
}
|
||||
|
||||
int AbstractSingleMediaPreview::previewTop() const {
|
||||
return _previewTop;
|
||||
}
|
||||
|
||||
int AbstractSingleMediaPreview::previewWidth() const {
|
||||
return _previewWidth;
|
||||
}
|
||||
|
||||
int AbstractSingleMediaPreview::previewHeight() const {
|
||||
return _previewHeight;
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::setAnimated(bool animated) {
|
||||
_animated = animated;
|
||||
}
|
||||
|
||||
bool AbstractSingleMediaPreview::isPhoto() const {
|
||||
return drawBackground()
|
||||
&& !isAnimatedPreviewReady()
|
||||
&& !_animated;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/chat/attach/attach_abstract_single_preview.h"
|
||||
#include "ui/chat/attach/attach_controls.h"
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/abstract_button.h"
|
||||
|
||||
namespace style {
|
||||
struct ComposeControls;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class PopupMenu;
|
||||
|
||||
class AbstractSingleMediaPreview : public AbstractSinglePreview {
|
||||
public:
|
||||
AbstractSingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
AttachControls::Type type,
|
||||
Fn<bool(AttachActionType)> actionAllowed);
|
||||
~AbstractSingleMediaPreview();
|
||||
|
||||
void setSendWay(SendFilesWay way);
|
||||
[[nodiscard]] SendFilesWay sendWay() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<> deleteRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> editRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> modifyRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> editCoverRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> clearCoverRequests() const override;
|
||||
|
||||
[[nodiscard]] bool isPhoto() const;
|
||||
|
||||
void setSpoiler(bool spoiler);
|
||||
[[nodiscard]] bool hasSpoiler() const;
|
||||
[[nodiscard]] bool canHaveSpoiler() const;
|
||||
[[nodiscard]] rpl::producer<bool> spoileredChanges() const;
|
||||
|
||||
[[nodiscard]] QImage generatePriceTagBackground() const;
|
||||
|
||||
protected:
|
||||
virtual bool supportsSpoilers() const = 0;
|
||||
virtual bool drawBackground() const = 0;
|
||||
virtual bool tryPaintAnimation(QPainter &p) = 0;
|
||||
virtual bool isAnimatedPreviewReady() const = 0;
|
||||
|
||||
void preparePreview(QImage preview);
|
||||
|
||||
int previewLeft() const;
|
||||
int previewTop() const;
|
||||
int previewWidth() const;
|
||||
int previewHeight() const;
|
||||
|
||||
void setAnimated(bool animated);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
[[nodiscard]] bool isOverPreview(QPoint position) const;
|
||||
void applyCursor(style::cursor cursor);
|
||||
void showContextMenu(QPoint position);
|
||||
|
||||
const style::ComposeControls &_st;
|
||||
SendFilesWay _sendWay;
|
||||
Fn<bool(AttachActionType)> _actionAllowed;
|
||||
bool _animated = false;
|
||||
QPixmap _preview;
|
||||
QPixmap _previewBlurred;
|
||||
int _previewLeft = 0;
|
||||
int _previewTop = 0;
|
||||
int _previewWidth = 0;
|
||||
int _previewHeight = 0;
|
||||
|
||||
std::unique_ptr<SpoilerAnimation> _spoiler;
|
||||
rpl::event_stream<bool> _spoileredChanges;
|
||||
|
||||
const int _minThumbH;
|
||||
const base::unique_qptr<AttachControlsWidget> _controls;
|
||||
rpl::event_stream<> _photoEditorRequests;
|
||||
rpl::event_stream<> _editCoverRequests;
|
||||
rpl::event_stream<> _clearCoverRequests;
|
||||
|
||||
style::cursor _cursor = style::cur_default;
|
||||
bool _pressed = false;
|
||||
|
||||
base::unique_qptr<PopupMenu> _menu;
|
||||
|
||||
rpl::event_stream<> _modifyRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class AbstractSinglePreview : public RpWidget {
|
||||
public:
|
||||
using RpWidget::RpWidget;
|
||||
|
||||
[[nodiscard]] virtual rpl::producer<> deleteRequests() const = 0;
|
||||
[[nodiscard]] virtual rpl::producer<> editRequests() const = 0;
|
||||
[[nodiscard]] virtual rpl::producer<> modifyRequests() const = 0;
|
||||
[[nodiscard]] virtual rpl::producer<> editCoverRequests() const = 0;
|
||||
[[nodiscard]] virtual rpl::producer<> clearCoverRequests() const = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
675
Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp
Normal file
675
Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp
Normal file
@@ -0,0 +1,675 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_album_preview.h"
|
||||
|
||||
#include "ui/chat/attach/attach_album_thumbnail.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
namespace Media::Streaming {
|
||||
|
||||
[[nodiscard]] QImage PrepareBlurredBackground(QSize outer, QImage frame);
|
||||
|
||||
} // namespace Media::Streaming
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDragDuration = crl::time(200);
|
||||
|
||||
} // namespace
|
||||
|
||||
AlbumPreview::AlbumPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
gsl::span<Ui::PreparedFile> items,
|
||||
SendFilesWay way,
|
||||
Fn<bool(int, AttachActionType)> actionAllowed)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _sendWay(way)
|
||||
, _actionAllowed(std::move(actionAllowed))
|
||||
, _dragTimer([=] { switchToDrag(); }) {
|
||||
setMouseTracking(true);
|
||||
prepareThumbs(items);
|
||||
updateSize();
|
||||
updateFileRows();
|
||||
}
|
||||
|
||||
AlbumPreview::~AlbumPreview() = default;
|
||||
|
||||
void AlbumPreview::setSendWay(SendFilesWay way) {
|
||||
if (_sendWay != way) {
|
||||
cancelDrag();
|
||||
_sendWay = way;
|
||||
}
|
||||
updateSize();
|
||||
updateFileRows();
|
||||
update();
|
||||
}
|
||||
|
||||
void AlbumPreview::updateFileRows() {
|
||||
Expects(_order.size() == _thumbs.size());
|
||||
|
||||
const auto isFile = !_sendWay.sendImagesAsPhotos();
|
||||
auto top = 0;
|
||||
for (auto i = 0; i < _order.size(); i++) {
|
||||
const auto &thumb = _thumbs[_order[i]];
|
||||
thumb->setButtonVisible(isFile && !thumb->isCompressedSticker());
|
||||
thumb->moveButtons(top);
|
||||
top += thumb->fileHeight() + st::sendMediaRowSkip;
|
||||
}
|
||||
}
|
||||
|
||||
base::flat_set<int> AlbumPreview::collectSpoileredIndices() {
|
||||
auto result = base::flat_set<int>();
|
||||
result.reserve(_thumbs.size());
|
||||
auto i = 0;
|
||||
for (const auto &thumb : _thumbs) {
|
||||
if (thumb->hasSpoiler()) {
|
||||
result.emplace(i);
|
||||
}
|
||||
++i;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool AlbumPreview::canHaveSpoiler(int index) const {
|
||||
return _sendWay.sendImagesAsPhotos();
|
||||
}
|
||||
|
||||
void AlbumPreview::toggleSpoilers(bool enabled) {
|
||||
for (auto &thumb : _thumbs) {
|
||||
thumb->setSpoiler(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<int> AlbumPreview::takeOrder() {
|
||||
//Expects(_thumbs.size() == _order.size());
|
||||
//Expects(_itemsShownDimensions.size() == _order.size());
|
||||
|
||||
auto reordered = std::vector<std::unique_ptr<AlbumThumbnail>>();
|
||||
auto reorderedShownDimensions = std::vector<QSize>();
|
||||
reordered.reserve(_thumbs.size());
|
||||
reorderedShownDimensions.reserve(_itemsShownDimensions.size());
|
||||
for (auto index : _order) {
|
||||
reordered.push_back(std::move(_thumbs[index]));
|
||||
reorderedShownDimensions.push_back(_itemsShownDimensions[index]);
|
||||
}
|
||||
_thumbs = std::move(reordered);
|
||||
_itemsShownDimensions = std::move(reorderedShownDimensions);
|
||||
return std::exchange(_order, defaultOrder());
|
||||
}
|
||||
|
||||
auto AlbumPreview::generateOrderedLayout() const
|
||||
-> std::vector<GroupMediaLayout> {
|
||||
auto layout = LayoutMediaGroup(
|
||||
_itemsShownDimensions,
|
||||
st::sendMediaPreviewSize,
|
||||
st::historyGroupWidthMin / 2,
|
||||
st::historyGroupSkip / 2);
|
||||
Assert(layout.size() == _order.size());
|
||||
return layout;
|
||||
}
|
||||
|
||||
std::vector<int> AlbumPreview::defaultOrder(int count) const {
|
||||
if (count < 0) {
|
||||
count = _order.size();
|
||||
}
|
||||
return ranges::views::ints(0, count) | ranges::to_vector;
|
||||
}
|
||||
|
||||
void AlbumPreview::prepareThumbs(gsl::span<Ui::PreparedFile> items) {
|
||||
_order = defaultOrder(items.size());
|
||||
_itemsShownDimensions = ranges::views::all(
|
||||
_order
|
||||
) | ranges::views::transform([&](int index) {
|
||||
return items[index].shownDimensions;
|
||||
}) | ranges::to_vector;
|
||||
|
||||
const auto count = int(_order.size());
|
||||
const auto layout = generateOrderedLayout();
|
||||
_thumbs.reserve(count);
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
_thumbs.push_back(std::make_unique<AlbumThumbnail>(
|
||||
_st,
|
||||
items[i],
|
||||
layout[i],
|
||||
this,
|
||||
[=] { update(); },
|
||||
[=] { changeThumbByIndex(orderIndex(thumbUnderCursor())); },
|
||||
[=] { deleteThumbByIndex(orderIndex(thumbUnderCursor())); }));
|
||||
if (_thumbs.back()->isCompressedSticker()) {
|
||||
_hasMixedFileHeights = true;
|
||||
}
|
||||
}
|
||||
_thumbsHeight = countLayoutHeight(layout);
|
||||
_photosHeight = ranges::accumulate(ranges::views::all(
|
||||
_thumbs
|
||||
) | ranges::views::transform([](const auto &thumb) {
|
||||
return thumb->photoHeight();
|
||||
}), 0) + (count - 1) * st::sendMediaRowSkip;
|
||||
|
||||
if (!_hasMixedFileHeights) {
|
||||
_filesHeight = count * _thumbs.front()->fileHeight()
|
||||
+ (count - 1) * st::sendMediaRowSkip;
|
||||
} else {
|
||||
_filesHeight = ranges::accumulate(ranges::views::all(
|
||||
_thumbs
|
||||
) | ranges::views::transform([](const auto &thumb) {
|
||||
return thumb->fileHeight();
|
||||
}), 0) + (count - 1) * st::sendMediaRowSkip;
|
||||
}
|
||||
}
|
||||
|
||||
int AlbumPreview::contentLeft() const {
|
||||
return (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
|
||||
}
|
||||
|
||||
int AlbumPreview::contentTop() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
AlbumThumbnail *AlbumPreview::findThumb(QPoint position) const {
|
||||
position -= QPoint(contentLeft(), contentTop());
|
||||
|
||||
auto top = 0;
|
||||
const auto isPhotosWay = _sendWay.sendImagesAsPhotos();
|
||||
const auto skip = st::sendMediaRowSkip;
|
||||
auto find = [&](const auto &thumb) {
|
||||
if (_sendWay.groupFiles() && _sendWay.sendImagesAsPhotos()) {
|
||||
return thumb->containsPoint(position);
|
||||
} else {
|
||||
const auto bottom = top + (isPhotosWay
|
||||
? thumb->photoHeight()
|
||||
: thumb->fileHeight());
|
||||
const auto isUnderTop = (position.y() > top);
|
||||
top = bottom + skip;
|
||||
return isUnderTop && (position.y() < bottom);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const auto i = ranges::find_if(_thumbs, std::move(find));
|
||||
return (i == _thumbs.end()) ? nullptr : i->get();
|
||||
}
|
||||
|
||||
not_null<AlbumThumbnail*> AlbumPreview::findClosestThumb(
|
||||
QPoint position) const {
|
||||
Expects(_draggedThumb != nullptr);
|
||||
|
||||
if (const auto exact = findThumb(position)) {
|
||||
return exact;
|
||||
}
|
||||
auto result = _draggedThumb;
|
||||
auto distance = _draggedThumb->distanceTo(position);
|
||||
for (const auto &thumb : _thumbs) {
|
||||
const auto check = thumb->distanceTo(position);
|
||||
if (check < distance) {
|
||||
distance = check;
|
||||
result = thumb.get();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int AlbumPreview::orderIndex(not_null<AlbumThumbnail*> thumb) const {
|
||||
const auto i = ranges::find_if(_order, [&](int index) {
|
||||
return (_thumbs[index].get() == thumb);
|
||||
});
|
||||
Assert(i != _order.end());
|
||||
return int(i - _order.begin());
|
||||
}
|
||||
|
||||
void AlbumPreview::cancelDrag() {
|
||||
_thumbsHeightAnimation.stop();
|
||||
_finishDragAnimation.stop();
|
||||
_shrinkAnimation.stop();
|
||||
if (_draggedThumb) {
|
||||
_draggedThumb->moveInAlbum({ 0, 0 });
|
||||
_draggedThumb = nullptr;
|
||||
}
|
||||
if (_suggestedThumb) {
|
||||
const auto suggestedIndex = orderIndex(_suggestedThumb);
|
||||
if (suggestedIndex > 0) {
|
||||
_thumbs[_order[suggestedIndex - 1]]->suggestMove(0., [] {});
|
||||
}
|
||||
if (suggestedIndex < int(_order.size() - 1)) {
|
||||
_thumbs[_order[suggestedIndex + 1]]->suggestMove(0., [] {});
|
||||
}
|
||||
_suggestedThumb->suggestMove(0., [] {});
|
||||
_suggestedThumb->finishAnimations();
|
||||
_suggestedThumb = nullptr;
|
||||
}
|
||||
_paintedAbove = nullptr;
|
||||
update();
|
||||
}
|
||||
|
||||
void AlbumPreview::finishDrag() {
|
||||
Expects(_draggedThumb != nullptr);
|
||||
Expects(_suggestedThumb != nullptr);
|
||||
|
||||
if (_suggestedThumb != _draggedThumb) {
|
||||
const auto currentIndex = orderIndex(_draggedThumb);
|
||||
const auto newIndex = orderIndex(_suggestedThumb);
|
||||
const auto delta = (currentIndex < newIndex) ? 1 : -1;
|
||||
const auto realIndex = _order[currentIndex];
|
||||
for (auto i = currentIndex; i != newIndex; i += delta) {
|
||||
_order[i] = _order[i + delta];
|
||||
}
|
||||
_order[newIndex] = realIndex;
|
||||
const auto layout = generateOrderedLayout();
|
||||
for (auto i = 0, count = int(_order.size()); i != count; ++i) {
|
||||
_thumbs[_order[i]]->moveToLayout(layout[i]);
|
||||
}
|
||||
_finishDragAnimation.start([=] { update(); }, 0., 1., kDragDuration);
|
||||
|
||||
updateSizeAnimated(layout);
|
||||
_orderUpdated.fire({});
|
||||
} else {
|
||||
for (const auto &thumb : _thumbs) {
|
||||
thumb->resetLayoutAnimation();
|
||||
}
|
||||
_draggedThumb->animateLayoutToInitial();
|
||||
_finishDragAnimation.start([=] { update(); }, 0., 1., kDragDuration);
|
||||
}
|
||||
}
|
||||
|
||||
int AlbumPreview::countLayoutHeight(
|
||||
const std::vector<GroupMediaLayout> &layout) const {
|
||||
const auto accumulator = [](int current, const auto &item) {
|
||||
return std::max(current, item.geometry.y() + item.geometry.height());
|
||||
};
|
||||
return ranges::accumulate(layout, 0, accumulator);
|
||||
}
|
||||
|
||||
void AlbumPreview::updateSizeAnimated(
|
||||
const std::vector<GroupMediaLayout> &layout) {
|
||||
const auto newHeight = countLayoutHeight(layout);
|
||||
if (newHeight != _thumbsHeight) {
|
||||
_thumbsHeightAnimation.start(
|
||||
[=] { updateSize(); },
|
||||
_thumbsHeight,
|
||||
newHeight,
|
||||
kDragDuration);
|
||||
_thumbsHeight = newHeight;
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::updateSize() {
|
||||
const auto newHeight = [&] {
|
||||
if (!_sendWay.sendImagesAsPhotos()) {
|
||||
return _filesHeight;
|
||||
} else if (!_sendWay.groupFiles()) {
|
||||
return _photosHeight;
|
||||
} else {
|
||||
return int(base::SafeRound(_thumbsHeightAnimation.value(
|
||||
_thumbsHeight)));
|
||||
}
|
||||
}();
|
||||
if (height() != newHeight) {
|
||||
resize(st::boxWideWidth, newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
if (!_sendWay.sendImagesAsPhotos()) {
|
||||
paintFiles(p, e->rect());
|
||||
} else if (!_sendWay.groupFiles()) {
|
||||
paintPhotos(p, e->rect());
|
||||
} else {
|
||||
paintAlbum(p);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::paintAlbum(Painter &p) const {
|
||||
const auto shrink = _shrinkAnimation.value(_draggedThumb ? 1. : 0.);
|
||||
const auto moveProgress = _finishDragAnimation.value(1.);
|
||||
const auto left = contentLeft();
|
||||
const auto top = contentTop();
|
||||
for (const auto &thumb : _thumbs) {
|
||||
if (thumb.get() != _paintedAbove) {
|
||||
thumb->paintInAlbum(p, left, top, shrink, moveProgress);
|
||||
}
|
||||
}
|
||||
if (_paintedAbove) {
|
||||
_paintedAbove->paintInAlbum(p, left, top, shrink, moveProgress);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::paintPhotos(Painter &p, QRect clip) const {
|
||||
const auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
|
||||
auto top = 0;
|
||||
const auto outerWidth = width();
|
||||
for (const auto &thumb : _thumbs) {
|
||||
const auto bottom = top + thumb->photoHeight();
|
||||
const auto guard = gsl::finally([&] {
|
||||
top = bottom + st::sendMediaRowSkip;
|
||||
});
|
||||
if (top >= clip.y() + clip.height()) {
|
||||
break;
|
||||
} else if (bottom <= clip.y()) {
|
||||
continue;
|
||||
}
|
||||
thumb->paintPhoto(p, left, top, outerWidth);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::paintFiles(Painter &p, QRect clip) const {
|
||||
const auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
|
||||
const auto outerWidth = width();
|
||||
if (!_hasMixedFileHeights) {
|
||||
const auto fileHeight = st::attachPreviewThumbLayout.thumbSize
|
||||
+ st::sendMediaRowSkip;
|
||||
const auto bottom = clip.y() + clip.height();
|
||||
const auto from = std::clamp(
|
||||
clip.y() / fileHeight,
|
||||
0,
|
||||
int(_thumbs.size()));
|
||||
const auto till = std::clamp(
|
||||
(bottom + fileHeight - 1) / fileHeight,
|
||||
0,
|
||||
int(_thumbs.size()));
|
||||
|
||||
auto top = from * fileHeight;
|
||||
for (auto i = from; i != till; ++i) {
|
||||
_thumbs[i]->paintFile(p, left, top, outerWidth);
|
||||
top += fileHeight;
|
||||
}
|
||||
} else {
|
||||
auto top = 0;
|
||||
for (const auto &thumb : _thumbs) {
|
||||
const auto bottom = top + thumb->fileHeight();
|
||||
const auto guard = gsl::finally([&] {
|
||||
top = bottom + st::sendMediaRowSkip;
|
||||
});
|
||||
if (top >= clip.y() + clip.height()) {
|
||||
break;
|
||||
} else if (bottom <= clip.y()) {
|
||||
continue;
|
||||
}
|
||||
thumb->paintFile(p, left, top, outerWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AlbumThumbnail *AlbumPreview::thumbUnderCursor() {
|
||||
return findThumb(mapFromGlobal(QCursor::pos()));
|
||||
}
|
||||
|
||||
void AlbumPreview::deleteThumbByIndex(int index) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
_thumbDeleted.fire(std::move(index));
|
||||
}
|
||||
|
||||
void AlbumPreview::changeThumbByIndex(int index) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
_thumbChanged.fire(std::move(index));
|
||||
}
|
||||
|
||||
void AlbumPreview::modifyThumbByIndex(int index) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
_thumbModified.fire(std::move(index));
|
||||
}
|
||||
|
||||
void AlbumPreview::thumbButtonsCallback(
|
||||
not_null<AlbumThumbnail*> thumb,
|
||||
AttachButtonType type) {
|
||||
const auto index = orderIndex(thumb);
|
||||
|
||||
switch (type) {
|
||||
case AttachButtonType::None: return;
|
||||
case AttachButtonType::Edit: changeThumbByIndex(index); break;
|
||||
case AttachButtonType::Delete: deleteThumbByIndex(index); break;
|
||||
case AttachButtonType::Modify:
|
||||
cancelDrag();
|
||||
modifyThumbByIndex(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::mousePressEvent(QMouseEvent *e) {
|
||||
if (_finishDragAnimation.animating()) {
|
||||
return;
|
||||
}
|
||||
const auto position = e->pos();
|
||||
cancelDrag();
|
||||
if (const auto thumb = findThumb(position)) {
|
||||
_draggedStartPosition = position;
|
||||
_pressedThumb = thumb;
|
||||
_pressedButtonType = thumb->buttonTypeFromPoint(position);
|
||||
|
||||
const auto isAlbum = _sendWay.sendImagesAsPhotos()
|
||||
&& _sendWay.groupFiles();
|
||||
if (!isAlbum || e->button() != Qt::LeftButton) {
|
||||
_dragTimer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pressedButtonType == AttachButtonType::None) {
|
||||
switchToDrag();
|
||||
} else if (_pressedButtonType == AttachButtonType::Modify) {
|
||||
_dragTimer.callOnce(QApplication::startDragTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::mouseMoveEvent(QMouseEvent *e) {
|
||||
if (!_sendWay.sendImagesAsPhotos() && !_hasMixedFileHeights) {
|
||||
applyCursor(style::cur_default);
|
||||
return;
|
||||
}
|
||||
if (_dragTimer.isActive()) {
|
||||
_dragTimer.cancel();
|
||||
switchToDrag();
|
||||
}
|
||||
const auto isAlbum = _sendWay.sendImagesAsPhotos()
|
||||
&& _sendWay.groupFiles();
|
||||
if (isAlbum && _draggedThumb) {
|
||||
const auto position = e->pos();
|
||||
_draggedThumb->moveInAlbum(position - _draggedStartPosition);
|
||||
updateSuggestedDrag(_draggedThumb->center());
|
||||
update();
|
||||
} else {
|
||||
const auto thumb = findThumb(e->pos());
|
||||
const auto regularCursor = isAlbum
|
||||
? style::cur_pointer
|
||||
: style::cur_default;
|
||||
const auto cursor = thumb
|
||||
? (thumb->buttonsContainPoint(e->pos())
|
||||
? style::cur_pointer
|
||||
: regularCursor)
|
||||
: style::cur_default;
|
||||
applyCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::applyCursor(style::cursor cursor) {
|
||||
if (_cursor != cursor) {
|
||||
_cursor = cursor;
|
||||
setCursor(_cursor);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::updateSuggestedDrag(QPoint position) {
|
||||
auto closest = findClosestThumb(position);
|
||||
auto closestIndex = orderIndex(closest);
|
||||
|
||||
const auto draggedIndex = orderIndex(_draggedThumb);
|
||||
const auto closestIsBeforePoint = closest->isPointAfter(position);
|
||||
if (closestIndex < draggedIndex && closestIsBeforePoint) {
|
||||
closest = _thumbs[_order[++closestIndex]].get();
|
||||
} else if (closestIndex > draggedIndex && !closestIsBeforePoint) {
|
||||
closest = _thumbs[_order[--closestIndex]].get();
|
||||
}
|
||||
|
||||
if (_suggestedThumb == closest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto last = int(_order.size()) - 1;
|
||||
if (_suggestedThumb) {
|
||||
const auto suggestedIndex = orderIndex(_suggestedThumb);
|
||||
if (suggestedIndex < draggedIndex && suggestedIndex > 0) {
|
||||
const auto previous = _thumbs[_order[suggestedIndex - 1]].get();
|
||||
previous->suggestMove(0., [=] { update(); });
|
||||
} else if (suggestedIndex > draggedIndex && suggestedIndex < last) {
|
||||
const auto next = _thumbs[_order[suggestedIndex + 1]].get();
|
||||
next->suggestMove(0., [=] { update(); });
|
||||
}
|
||||
_suggestedThumb->suggestMove(0., [=] { update(); });
|
||||
}
|
||||
_suggestedThumb = closest;
|
||||
const auto suggestedIndex = closestIndex;
|
||||
if (_suggestedThumb != _draggedThumb) {
|
||||
const auto delta = (suggestedIndex < draggedIndex) ? 1. : -1.;
|
||||
if (delta > 0. && suggestedIndex > 0) {
|
||||
const auto previous = _thumbs[_order[suggestedIndex - 1]].get();
|
||||
previous->suggestMove(-delta, [=] { update(); });
|
||||
} else if (delta < 0. && suggestedIndex < last) {
|
||||
const auto next = _thumbs[_order[suggestedIndex + 1]].get();
|
||||
next->suggestMove(-delta, [=] { update(); });
|
||||
}
|
||||
_suggestedThumb->suggestMove(delta, [=] { update(); });
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::mouseReleaseEvent(QMouseEvent *e) {
|
||||
if (_draggedThumb) {
|
||||
finishDrag();
|
||||
_shrinkAnimation.start(
|
||||
[=] { update(); },
|
||||
1.,
|
||||
0.,
|
||||
AlbumThumbnail::kShrinkDuration);
|
||||
_draggedThumb = nullptr;
|
||||
_suggestedThumb = nullptr;
|
||||
update();
|
||||
} else if (const auto thumb = base::take(_pressedThumb)) {
|
||||
const auto was = _pressedButtonType;
|
||||
const auto now = thumb->buttonTypeFromPoint(e->pos());
|
||||
if (e->button() == Qt::RightButton) {
|
||||
showContextMenu(thumb, e->globalPos());
|
||||
} else if (was == now) {
|
||||
thumbButtonsCallback(thumb, now);
|
||||
}
|
||||
}
|
||||
_pressedButtonType = AttachButtonType::None;
|
||||
}
|
||||
|
||||
void AlbumPreview::showContextMenu(
|
||||
not_null<AlbumThumbnail*> thumb,
|
||||
QPoint position) {
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
st::popupMenuWithIcons);
|
||||
|
||||
const auto index = orderIndex(thumb);
|
||||
if (_actionAllowed(index, AttachActionType::ToggleSpoiler)
|
||||
&& _sendWay.sendImagesAsPhotos()) {
|
||||
const auto spoilered = thumb->hasSpoiler();
|
||||
_menu->addAction(spoilered
|
||||
? tr::lng_context_disable_spoiler(tr::now)
|
||||
: tr::lng_context_spoiler_effect(tr::now), [=] {
|
||||
thumb->setSpoiler(!spoilered);
|
||||
}, spoilered ? &st::menuIconSpoilerOff : &st::menuIconSpoiler);
|
||||
}
|
||||
if (_actionAllowed(index, AttachActionType::EditCover)) {
|
||||
_menu->addAction(tr::lng_context_edit_cover(tr::now), [=] {
|
||||
_thumbEditCoverRequested.fire_copy(index);
|
||||
}, &st::menuIconEdit);
|
||||
|
||||
if (_actionAllowed(index, AttachActionType::ClearCover)) {
|
||||
_menu->addAction(tr::lng_context_clear_cover(tr::now), [=] {
|
||||
_thumbClearCoverRequested.fire_copy(index);
|
||||
}, &st::menuIconCancel);
|
||||
}
|
||||
}
|
||||
|
||||
if (_menu->empty()) {
|
||||
_menu = nullptr;
|
||||
} else {
|
||||
_menu->popup(position);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::switchToDrag() {
|
||||
_paintedAbove
|
||||
= _suggestedThumb
|
||||
= _draggedThumb
|
||||
= base::take(_pressedThumb);
|
||||
_shrinkAnimation.start(
|
||||
[=] { update(); },
|
||||
0.,
|
||||
1.,
|
||||
AlbumThumbnail::kShrinkDuration);
|
||||
applyCursor(style::cur_sizeall);
|
||||
update();
|
||||
}
|
||||
|
||||
QImage AlbumPreview::generatePriceTagBackground() const {
|
||||
auto wmax = 0;
|
||||
auto hmax = 0;
|
||||
for (auto &thumb : _thumbs) {
|
||||
const auto geometry = thumb->geometry();
|
||||
accumulate_max(wmax, geometry.x() + geometry.width());
|
||||
accumulate_max(hmax, geometry.y() + geometry.height());
|
||||
}
|
||||
const auto size = QSize(wmax, hmax);
|
||||
if (size.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto full = size * ratio;
|
||||
const auto skip = st::historyGroupSkip;
|
||||
auto result = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(ratio);
|
||||
result.fill(Qt::black);
|
||||
auto p = QPainter(&result);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
for (auto &thumb : _thumbs) {
|
||||
const auto geometry = thumb->geometry();
|
||||
if (geometry.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const auto w = geometry.width();
|
||||
const auto h = geometry.height();
|
||||
const auto wscale = (w + skip) / float64(w);
|
||||
const auto hscale = (h + skip) / float64(h);
|
||||
p.save();
|
||||
p.translate(geometry.center());
|
||||
p.scale(wscale, hscale);
|
||||
p.translate(-geometry.center());
|
||||
thumb->paintInAlbum(p, 0, 0, 1., 1.);
|
||||
p.restore();
|
||||
}
|
||||
p.end();
|
||||
|
||||
return ::Media::Streaming::PrepareBlurredBackground(
|
||||
full,
|
||||
std::move(result));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
142
Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h
Normal file
142
Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
namespace style {
|
||||
struct ComposeControls;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct PreparedFile;
|
||||
struct GroupMediaLayout;
|
||||
class AlbumThumbnail;
|
||||
class PopupMenu;
|
||||
|
||||
class AlbumPreview final : public RpWidget {
|
||||
public:
|
||||
AlbumPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
gsl::span<Ui::PreparedFile> items,
|
||||
SendFilesWay way,
|
||||
Fn<bool(int, AttachActionType)> actionAllowed);
|
||||
~AlbumPreview();
|
||||
|
||||
void setSendWay(SendFilesWay way);
|
||||
|
||||
[[nodiscard]] base::flat_set<int> collectSpoileredIndices();
|
||||
[[nodiscard]] bool canHaveSpoiler(int index) const;
|
||||
void toggleSpoilers(bool enabled);
|
||||
[[nodiscard]] std::vector<int> takeOrder();
|
||||
|
||||
[[nodiscard]] rpl::producer<int> thumbDeleted() const {
|
||||
return _thumbDeleted.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<int> thumbChanged() const {
|
||||
return _thumbChanged.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<int> thumbModified() const {
|
||||
return _thumbModified.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<int> thumbEditCoverRequested() const {
|
||||
return _thumbEditCoverRequested.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<int> thumbClearCoverRequested() const {
|
||||
return _thumbClearCoverRequested.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> orderUpdated() const {
|
||||
return _orderUpdated.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage generatePriceTagBackground() const;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
int countLayoutHeight(
|
||||
const std::vector<GroupMediaLayout> &layout) const;
|
||||
std::vector<GroupMediaLayout> generateOrderedLayout() const;
|
||||
std::vector<int> defaultOrder(int count = -1) const;
|
||||
void prepareThumbs(gsl::span<Ui::PreparedFile> items);
|
||||
void updateSizeAnimated(const std::vector<GroupMediaLayout> &layout);
|
||||
void updateSize();
|
||||
void updateFileRows();
|
||||
|
||||
AlbumThumbnail *thumbUnderCursor();
|
||||
void deleteThumbByIndex(int index);
|
||||
void changeThumbByIndex(int index);
|
||||
void modifyThumbByIndex(int index);
|
||||
void thumbButtonsCallback(
|
||||
not_null<AlbumThumbnail*> thumb,
|
||||
AttachButtonType type);
|
||||
|
||||
void switchToDrag();
|
||||
|
||||
void paintAlbum(Painter &p) const;
|
||||
void paintPhotos(Painter &p, QRect clip) const;
|
||||
void paintFiles(Painter &p, QRect clip) const;
|
||||
|
||||
void applyCursor(style::cursor cursor);
|
||||
int contentLeft() const;
|
||||
int contentTop() const;
|
||||
AlbumThumbnail *findThumb(QPoint position) const;
|
||||
not_null<AlbumThumbnail*> findClosestThumb(QPoint position) const;
|
||||
void updateSuggestedDrag(QPoint position);
|
||||
int orderIndex(not_null<AlbumThumbnail*> thumb) const;
|
||||
void cancelDrag();
|
||||
void finishDrag();
|
||||
|
||||
void showContextMenu(not_null<AlbumThumbnail*> thumb, QPoint position);
|
||||
|
||||
const style::ComposeControls &_st;
|
||||
SendFilesWay _sendWay;
|
||||
Fn<bool(int, AttachActionType)> _actionAllowed;
|
||||
style::cursor _cursor = style::cur_default;
|
||||
std::vector<int> _order;
|
||||
std::vector<QSize> _itemsShownDimensions;
|
||||
std::vector<std::unique_ptr<AlbumThumbnail>> _thumbs;
|
||||
int _thumbsHeight = 0;
|
||||
int _photosHeight = 0;
|
||||
int _filesHeight = 0;
|
||||
|
||||
bool _hasMixedFileHeights = false;
|
||||
|
||||
AlbumThumbnail *_draggedThumb = nullptr;
|
||||
AlbumThumbnail *_suggestedThumb = nullptr;
|
||||
AlbumThumbnail *_paintedAbove = nullptr;
|
||||
AlbumThumbnail *_pressedThumb = nullptr;
|
||||
QPoint _draggedStartPosition;
|
||||
|
||||
base::Timer _dragTimer;
|
||||
AttachButtonType _pressedButtonType = AttachButtonType::None;
|
||||
|
||||
rpl::event_stream<int> _thumbDeleted;
|
||||
rpl::event_stream<int> _thumbChanged;
|
||||
rpl::event_stream<int> _thumbModified;
|
||||
rpl::event_stream<int> _thumbEditCoverRequested;
|
||||
rpl::event_stream<int> _thumbClearCoverRequested;
|
||||
rpl::event_stream<> _orderUpdated;
|
||||
|
||||
base::unique_qptr<PopupMenu> _menu;
|
||||
|
||||
mutable Animations::Simple _thumbsHeightAnimation;
|
||||
mutable Animations::Simple _shrinkAnimation;
|
||||
mutable Animations::Simple _finishDragAnimation;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
626
Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp
Normal file
626
Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp
Normal file
@@ -0,0 +1,626 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_album_thumbnail.h"
|
||||
|
||||
#include "core/mime_type.h" // Core::IsMimeSticker.
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
#include <QtCore/QFileInfo>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
AlbumThumbnail::AlbumThumbnail(
|
||||
const style::ComposeControls &st,
|
||||
const PreparedFile &file,
|
||||
const GroupMediaLayout &layout,
|
||||
QWidget *parent,
|
||||
Fn<void()> repaint,
|
||||
Fn<void()> editCallback,
|
||||
Fn<void()> deleteCallback)
|
||||
: _st(st)
|
||||
, _layout(layout)
|
||||
, _fullPreview(file.videoCover ? file.videoCover->preview : file.preview)
|
||||
, _shrinkSize(int(std::ceil(st::roundRadiusLarge / 1.4)))
|
||||
, _isPhoto(file.type == PreparedFile::Type::Photo)
|
||||
, _isVideo(file.type == PreparedFile::Type::Video)
|
||||
, _isCompressedSticker(Core::IsMimeSticker(file.information->filemime))
|
||||
, _repaint(std::move(repaint)) {
|
||||
Expects(!_fullPreview.isNull());
|
||||
|
||||
moveToLayout(layout);
|
||||
|
||||
using Option = Images::Option;
|
||||
const auto previewWidth = _fullPreview.width();
|
||||
const auto previewHeight = _fullPreview.height();
|
||||
const auto imageWidth = std::max(
|
||||
previewWidth / style::DevicePixelRatio(),
|
||||
st::minPhotoSize);
|
||||
const auto imageHeight = std::max(
|
||||
previewHeight / style::DevicePixelRatio(),
|
||||
st::minPhotoSize);
|
||||
_photo = PixmapFromImage(Images::Prepare(
|
||||
_fullPreview,
|
||||
QSize(previewWidth, previewHeight),
|
||||
{
|
||||
.options = Option::RoundLarge,
|
||||
.outer = { imageWidth, imageHeight },
|
||||
}));
|
||||
|
||||
const auto &layoutSt = st::attachPreviewThumbLayout;
|
||||
const auto idealSize = layoutSt.thumbSize * style::DevicePixelRatio();
|
||||
const auto fileThumbSize = (previewWidth > previewHeight)
|
||||
? QSize(previewWidth * idealSize / previewHeight, idealSize)
|
||||
: QSize(idealSize, previewHeight * idealSize / previewWidth);
|
||||
_fileThumb = PixmapFromImage(Images::Prepare(
|
||||
_fullPreview,
|
||||
fileThumbSize,
|
||||
{
|
||||
.options = Option::RoundSmall,
|
||||
.outer = { layoutSt.thumbSize, layoutSt.thumbSize },
|
||||
}));
|
||||
|
||||
const auto availableFileWidth = st::sendMediaPreviewSize
|
||||
- layoutSt.thumbSize
|
||||
- layoutSt.thumbSkip
|
||||
// Right buttons.
|
||||
- st::sendBoxAlbumGroupButtonFile.width * 2
|
||||
- st::sendBoxAlbumGroupEditInternalSkip * 2
|
||||
- st::sendBoxAlbumGroupSkipRight;
|
||||
const auto filepath = file.path;
|
||||
if (filepath.isEmpty()) {
|
||||
_name = "image.png";
|
||||
_status = FormatImageSizeText(file.originalDimensions);
|
||||
} else {
|
||||
auto fileinfo = QFileInfo(filepath);
|
||||
_name = fileinfo.fileName();
|
||||
_status = FormatSizeText(fileinfo.size());
|
||||
}
|
||||
_nameWidth = st::semiboldFont->width(_name);
|
||||
if (_nameWidth > availableFileWidth) {
|
||||
_name = st::semiboldFont->elided(
|
||||
_name,
|
||||
availableFileWidth,
|
||||
Qt::ElideMiddle);
|
||||
_nameWidth = st::semiboldFont->width(_name);
|
||||
}
|
||||
_statusWidth = st::normalFont->width(_status);
|
||||
|
||||
_editMedia.create(parent, _st.files.buttonFile);
|
||||
_deleteMedia.create(parent, _st.files.buttonFile);
|
||||
|
||||
const auto duration = st::historyAttach.ripple.hideDuration;
|
||||
_editMedia->setClickedCallback([=] {
|
||||
base::call_delayed(duration, parent, editCallback);
|
||||
});
|
||||
_deleteMedia->setClickedCallback(deleteCallback);
|
||||
|
||||
_editMedia->setIconOverride(&_st.files.buttonFileEdit);
|
||||
_deleteMedia->setIconOverride(&_st.files.buttonFileDelete);
|
||||
|
||||
setSpoiler(file.spoiler);
|
||||
setButtonVisible(false);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::setSpoiler(bool spoiler) {
|
||||
Expects(_repaint != nullptr);
|
||||
|
||||
_spoiler = spoiler
|
||||
? std::make_unique<SpoilerAnimation>(_repaint)
|
||||
: nullptr;
|
||||
_repaint();
|
||||
}
|
||||
|
||||
bool AlbumThumbnail::hasSpoiler() const {
|
||||
return _spoiler != nullptr;
|
||||
}
|
||||
|
||||
void AlbumThumbnail::setButtonVisible(bool value) {
|
||||
_editMedia->setVisible(value);
|
||||
_deleteMedia->setVisible(value);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::moveButtons(int thumbTop) {
|
||||
const auto top = thumbTop + st::sendBoxFileGroupSkipTop;
|
||||
|
||||
auto right = st::sendBoxFileGroupSkipRight + st::boxPhotoPadding.right();
|
||||
_deleteMedia->moveToRight(right, top);
|
||||
right += st::sendBoxFileGroupEditInternalSkip + _deleteMedia->width();
|
||||
_editMedia->moveToRight(right, top);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::resetLayoutAnimation() {
|
||||
_animateFromGeometry = std::nullopt;
|
||||
}
|
||||
|
||||
void AlbumThumbnail::animateLayoutToInitial() {
|
||||
_animateFromGeometry = countRealGeometry();
|
||||
_suggestedMove = 0.;
|
||||
_albumPosition = QPoint(0, 0);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::moveToLayout(const GroupMediaLayout &layout) {
|
||||
using namespace Images;
|
||||
|
||||
animateLayoutToInitial();
|
||||
_layout = layout;
|
||||
|
||||
const auto width = _layout.geometry.width();
|
||||
const auto height = _layout.geometry.height();
|
||||
_albumCorners = GetCornersFromSides(_layout.sides);
|
||||
const auto pixSize = GetImageScaleSizeForGeometry(
|
||||
{ _fullPreview.width(), _fullPreview.height() },
|
||||
{ width, height });
|
||||
const auto pixWidth = pixSize.width() * style::DevicePixelRatio();
|
||||
const auto pixHeight = pixSize.height() * style::DevicePixelRatio();
|
||||
|
||||
_albumImage = PixmapFromImage(Prepare(
|
||||
_fullPreview,
|
||||
QSize(pixWidth, pixHeight),
|
||||
{
|
||||
.options = RoundOptions(ImageRoundRadius::Large, _albumCorners),
|
||||
.outer = { width, height },
|
||||
}));
|
||||
_albumImageBlurred = QPixmap();
|
||||
}
|
||||
|
||||
int AlbumThumbnail::photoHeight() const {
|
||||
return _photo.height() / style::DevicePixelRatio();
|
||||
}
|
||||
|
||||
int AlbumThumbnail::fileHeight() const {
|
||||
return _isCompressedSticker
|
||||
? photoHeight()
|
||||
: st::attachPreviewThumbLayout.thumbSize;
|
||||
}
|
||||
|
||||
bool AlbumThumbnail::isCompressedSticker() const {
|
||||
return _isCompressedSticker;
|
||||
}
|
||||
|
||||
void AlbumThumbnail::paintInAlbum(
|
||||
QPainter &p,
|
||||
int left,
|
||||
int top,
|
||||
float64 shrinkProgress,
|
||||
float64 moveProgress) {
|
||||
const auto shrink = anim::interpolate(0, _shrinkSize, shrinkProgress);
|
||||
_lastShrinkValue = shrink;
|
||||
const auto geometry = countCurrentGeometry(
|
||||
moveProgress
|
||||
).translated(left, top);
|
||||
auto paintedTo = geometry;
|
||||
const auto revealed = _spoiler ? shrinkProgress : 1.;
|
||||
if (revealed > 0.) {
|
||||
if (shrink > 0 || moveProgress < 1.) {
|
||||
const auto size = geometry.size();
|
||||
paintedTo = geometry.marginsRemoved(
|
||||
{ shrink, shrink, shrink, shrink }
|
||||
);
|
||||
if (shrinkProgress < 1 && _albumCorners != RectPart::None) {
|
||||
prepareCache(size, shrink);
|
||||
p.drawImage(geometry.topLeft(), _albumCache);
|
||||
} else {
|
||||
drawSimpleFrame(p, paintedTo, size);
|
||||
}
|
||||
} else {
|
||||
p.drawPixmap(geometry.topLeft(), _albumImage);
|
||||
}
|
||||
if (_isVideo) {
|
||||
paintPlayVideo(p, geometry);
|
||||
}
|
||||
}
|
||||
if (revealed < 1.) {
|
||||
auto corners = Images::CornersMaskRef(
|
||||
Images::CornersMask(ImageRoundRadius::Large));
|
||||
if (!(_albumCorners & RectPart::TopLeft)) {
|
||||
corners.p[0] = nullptr;
|
||||
}
|
||||
if (!(_albumCorners & RectPart::TopRight)) {
|
||||
corners.p[1] = nullptr;
|
||||
}
|
||||
if (!(_albumCorners & RectPart::BottomLeft)) {
|
||||
corners.p[2] = nullptr;
|
||||
}
|
||||
if (!(_albumCorners & RectPart::BottomRight)) {
|
||||
corners.p[3] = nullptr;
|
||||
}
|
||||
p.setOpacity(1. - revealed);
|
||||
if (_albumImageBlurred.isNull()) {
|
||||
_albumImageBlurred = BlurredPreviewFromPixmap(
|
||||
_albumImage,
|
||||
_albumCorners);
|
||||
}
|
||||
p.drawPixmap(paintedTo, _albumImageBlurred);
|
||||
const auto paused = On(PowerSaving::kChatSpoiler);
|
||||
FillSpoilerRect(
|
||||
p,
|
||||
paintedTo,
|
||||
corners,
|
||||
DefaultImageSpoiler().frame(_spoiler->index(crl::now(), paused)),
|
||||
_cornerCache);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
|
||||
_lastRectOfButtons = paintButtons(
|
||||
p,
|
||||
geometry,
|
||||
shrinkProgress);
|
||||
_lastRectOfModify = geometry;
|
||||
}
|
||||
|
||||
void AlbumThumbnail::paintPlayVideo(QPainter &p, QRect geometry) {
|
||||
const auto innerSize = st::msgFileLayout.thumbSize;
|
||||
const auto inner = QRect(
|
||||
geometry.x() + (geometry.width() - innerSize) / 2,
|
||||
geometry.y() + (geometry.height() - innerSize) / 2,
|
||||
innerSize,
|
||||
innerSize);
|
||||
{
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::msgDateImgBg);
|
||||
p.drawEllipse(inner);
|
||||
}
|
||||
st::historyFileThumbPlay.paintInCenter(p, inner);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::prepareCache(QSize size, int shrink) {
|
||||
const auto width = std::max(
|
||||
_layout.geometry.width(),
|
||||
_animateFromGeometry ? _animateFromGeometry->width() : 0);
|
||||
const auto height = std::max(
|
||||
_layout.geometry.height(),
|
||||
_animateFromGeometry ? _animateFromGeometry->height() : 0);
|
||||
const auto cacheSize = QSize(width, height) * style::DevicePixelRatio();
|
||||
|
||||
if (_albumCache.width() < cacheSize.width()
|
||||
|| _albumCache.height() < cacheSize.height()) {
|
||||
_albumCache = QImage(cacheSize, QImage::Format_ARGB32_Premultiplied);
|
||||
_albumCache.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
}
|
||||
_albumCache.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&_albumCache);
|
||||
const auto to = QRect(QPoint(), size).marginsRemoved(
|
||||
{ shrink, shrink, shrink, shrink }
|
||||
);
|
||||
drawSimpleFrame(p, to, size);
|
||||
}
|
||||
_albumCache = Images::Round(
|
||||
std::move(_albumCache),
|
||||
ImageRoundRadius::Large,
|
||||
_albumCorners,
|
||||
QRect(QPoint(), size * style::DevicePixelRatio()));
|
||||
}
|
||||
|
||||
void AlbumThumbnail::drawSimpleFrame(QPainter &p, QRect to, QSize size) const {
|
||||
const auto fullWidth = _fullPreview.width();
|
||||
const auto fullHeight = _fullPreview.height();
|
||||
const auto previewSize = GetImageScaleSizeForGeometry(
|
||||
{ fullWidth, fullHeight },
|
||||
{ size.width(), size.height() });
|
||||
const auto previewWidth = previewSize.width() * style::DevicePixelRatio();
|
||||
const auto previewHeight = previewSize.height() * style::DevicePixelRatio();
|
||||
const auto width = size.width() * style::DevicePixelRatio();
|
||||
const auto height = size.height() * style::DevicePixelRatio();
|
||||
const auto scaleWidth = to.width() / float64(width);
|
||||
const auto scaleHeight = to.height() / float64(height);
|
||||
const auto Round = [](float64 value) {
|
||||
return int(base::SafeRound(value));
|
||||
};
|
||||
const auto &[from, fillBlack] = [&] {
|
||||
if (previewWidth < width && previewHeight < height) {
|
||||
const auto toWidth = Round(previewWidth * scaleWidth);
|
||||
const auto toHeight = Round(previewHeight * scaleHeight);
|
||||
return std::make_pair(
|
||||
QRect(0, 0, fullWidth, fullHeight),
|
||||
QMargins(
|
||||
(to.width() - toWidth) / 2,
|
||||
(to.height() - toHeight) / 2,
|
||||
to.width() - toWidth - (to.width() - toWidth) / 2,
|
||||
to.height() - toHeight - (to.height() - toHeight) / 2));
|
||||
} else if (previewWidth * height > previewHeight * width) {
|
||||
if (previewHeight >= height) {
|
||||
const auto takeWidth = previewWidth * height / previewHeight;
|
||||
const auto useWidth = fullWidth * width / takeWidth;
|
||||
return std::make_pair(
|
||||
QRect(
|
||||
(fullWidth - useWidth) / 2,
|
||||
0,
|
||||
useWidth,
|
||||
fullHeight),
|
||||
QMargins(0, 0, 0, 0));
|
||||
} else {
|
||||
const auto takeWidth = previewWidth;
|
||||
const auto useWidth = fullWidth * width / takeWidth;
|
||||
const auto toHeight = Round(previewHeight * scaleHeight);
|
||||
const auto toSkip = (to.height() - toHeight) / 2;
|
||||
return std::make_pair(
|
||||
QRect(
|
||||
(fullWidth - useWidth) / 2,
|
||||
0,
|
||||
useWidth,
|
||||
fullHeight),
|
||||
QMargins(
|
||||
0,
|
||||
toSkip,
|
||||
0,
|
||||
to.height() - toHeight - toSkip));
|
||||
}
|
||||
} else {
|
||||
if (previewWidth >= width) {
|
||||
const auto takeHeight = previewHeight * width / previewWidth;
|
||||
const auto useHeight = fullHeight * height / takeHeight;
|
||||
return std::make_pair(
|
||||
QRect(
|
||||
0,
|
||||
(fullHeight - useHeight) / 2,
|
||||
fullWidth,
|
||||
useHeight),
|
||||
QMargins(0, 0, 0, 0));
|
||||
} else {
|
||||
const auto takeHeight = previewHeight;
|
||||
const auto useHeight = fullHeight * height / takeHeight;
|
||||
const auto toWidth = Round(previewWidth * scaleWidth);
|
||||
const auto toSkip = (to.width() - toWidth) / 2;
|
||||
return std::make_pair(
|
||||
QRect(
|
||||
0,
|
||||
(fullHeight - useHeight) / 2,
|
||||
fullWidth,
|
||||
useHeight),
|
||||
QMargins(
|
||||
toSkip,
|
||||
0,
|
||||
to.width() - toWidth - toSkip,
|
||||
0));
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
p.drawImage(to.marginsRemoved(fillBlack), _fullPreview, from);
|
||||
if (fillBlack.top() > 0) {
|
||||
p.fillRect(to.x(), to.y(), to.width(), fillBlack.top(), st::imageBg);
|
||||
}
|
||||
if (fillBlack.bottom() > 0) {
|
||||
p.fillRect(
|
||||
to.x(),
|
||||
to.y() + to.height() - fillBlack.bottom(),
|
||||
to.width(),
|
||||
fillBlack.bottom(),
|
||||
st::imageBg);
|
||||
}
|
||||
if (fillBlack.left() > 0) {
|
||||
p.fillRect(
|
||||
to.x(),
|
||||
to.y() + fillBlack.top(),
|
||||
fillBlack.left(),
|
||||
to.height() - fillBlack.top() - fillBlack.bottom(),
|
||||
st::imageBg);
|
||||
}
|
||||
if (fillBlack.right() > 0) {
|
||||
p.fillRect(
|
||||
to.x() + to.width() - fillBlack.right(),
|
||||
to.y() + fillBlack.top(),
|
||||
fillBlack.right(),
|
||||
to.height() - fillBlack.top() - fillBlack.bottom(),
|
||||
st::imageBg);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumThumbnail::paintPhoto(Painter &p, int left, int top, int outerWidth) {
|
||||
const auto size = _photo.size() / style::DevicePixelRatio();
|
||||
if (_spoiler && _photoBlurred.isNull()) {
|
||||
_photoBlurred = BlurredPreviewFromPixmap(
|
||||
_photo,
|
||||
RectPart::AllCorners);
|
||||
}
|
||||
const auto &pixmap = _spoiler ? _photoBlurred : _photo;
|
||||
const auto rect = QRect(
|
||||
left + (st::sendMediaPreviewSize - size.width()) / 2,
|
||||
top,
|
||||
pixmap.width() / pixmap.devicePixelRatio(),
|
||||
pixmap.height() / pixmap.devicePixelRatio());
|
||||
p.drawPixmapLeft(
|
||||
left + (st::sendMediaPreviewSize - size.width()) / 2,
|
||||
top,
|
||||
outerWidth,
|
||||
pixmap);
|
||||
if (_spoiler) {
|
||||
const auto paused = On(PowerSaving::kChatSpoiler);
|
||||
FillSpoilerRect(
|
||||
p,
|
||||
rect,
|
||||
Images::CornersMaskRef(
|
||||
Images::CornersMask(ImageRoundRadius::Large)),
|
||||
DefaultImageSpoiler().frame(_spoiler->index(crl::now(), paused)),
|
||||
_cornerCache);
|
||||
} else if (_isVideo) {
|
||||
paintPlayVideo(p, rect);
|
||||
}
|
||||
|
||||
const auto topLeft = QPoint{ left, top };
|
||||
|
||||
_lastRectOfButtons = paintButtons(
|
||||
p,
|
||||
QRect(left, top, st::sendMediaPreviewSize, size.height()),
|
||||
0);
|
||||
|
||||
_lastRectOfModify = QRect(topLeft, size);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::paintFile(
|
||||
Painter &p,
|
||||
int left,
|
||||
int top,
|
||||
int outerWidth) {
|
||||
|
||||
if (isCompressedSticker()) {
|
||||
auto spoiler = base::take(_spoiler);
|
||||
paintPhoto(p, left, top, outerWidth);
|
||||
_spoiler = base::take(spoiler);
|
||||
return;
|
||||
}
|
||||
const auto &st = st::attachPreviewThumbLayout;
|
||||
const auto textLeft = left + st.thumbSize + st.thumbSkip;
|
||||
|
||||
p.drawPixmap(left, top, _fileThumb);
|
||||
p.setFont(st::semiboldFont);
|
||||
p.setPen(_st.files.nameFg);
|
||||
p.drawTextLeft(
|
||||
textLeft,
|
||||
top + st.nameTop,
|
||||
outerWidth,
|
||||
_name,
|
||||
_nameWidth);
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(_st.files.statusFg);
|
||||
p.drawTextLeft(
|
||||
textLeft,
|
||||
top + st.statusTop,
|
||||
outerWidth,
|
||||
_status,
|
||||
_statusWidth);
|
||||
|
||||
_lastRectOfModify = QRect(
|
||||
QPoint(left, top),
|
||||
_fileThumb.size() / style::DevicePixelRatio());
|
||||
}
|
||||
|
||||
QRect AlbumThumbnail::geometry() const {
|
||||
return _layout.geometry;
|
||||
}
|
||||
|
||||
bool AlbumThumbnail::containsPoint(QPoint position) const {
|
||||
return _layout.geometry.contains(position);
|
||||
}
|
||||
|
||||
bool AlbumThumbnail::buttonsContainPoint(QPoint position) const {
|
||||
return ((_isPhoto && !_isCompressedSticker)
|
||||
? _lastRectOfModify
|
||||
: _lastRectOfButtons).contains(position);
|
||||
}
|
||||
|
||||
AttachButtonType AlbumThumbnail::buttonTypeFromPoint(QPoint position) const {
|
||||
if (!buttonsContainPoint(position)) {
|
||||
return AttachButtonType::None;
|
||||
}
|
||||
return (!_lastRectOfButtons.contains(position) && !_isCompressedSticker)
|
||||
? AttachButtonType::Modify
|
||||
: (_buttons.vertical()
|
||||
? (position.y() < _lastRectOfButtons.center().y())
|
||||
: (position.x() < _lastRectOfButtons.center().x()))
|
||||
? AttachButtonType::Edit
|
||||
: AttachButtonType::Delete;
|
||||
}
|
||||
|
||||
int AlbumThumbnail::distanceTo(QPoint position) const {
|
||||
const auto delta = (_layout.geometry.center() - position);
|
||||
return QPoint::dotProduct(delta, delta);
|
||||
}
|
||||
|
||||
bool AlbumThumbnail::isPointAfter(QPoint position) const {
|
||||
return position.x() > _layout.geometry.center().x();
|
||||
}
|
||||
|
||||
void AlbumThumbnail::moveInAlbum(QPoint to) {
|
||||
_albumPosition = to;
|
||||
}
|
||||
|
||||
QPoint AlbumThumbnail::center() const {
|
||||
auto realGeometry = _layout.geometry;
|
||||
realGeometry.moveTopLeft(realGeometry.topLeft() + _albumPosition);
|
||||
return realGeometry.center();
|
||||
}
|
||||
|
||||
void AlbumThumbnail::suggestMove(float64 delta, Fn<void()> callback) {
|
||||
if (_suggestedMove != delta) {
|
||||
_suggestedMoveAnimation.start(
|
||||
std::move(callback),
|
||||
_suggestedMove,
|
||||
delta,
|
||||
kShrinkDuration);
|
||||
_suggestedMove = delta;
|
||||
}
|
||||
}
|
||||
|
||||
QRect AlbumThumbnail::countRealGeometry() const {
|
||||
const auto addLeft = int(base::SafeRound(
|
||||
_suggestedMoveAnimation.value(_suggestedMove) * _lastShrinkValue));
|
||||
const auto current = _layout.geometry;
|
||||
const auto realTopLeft = current.topLeft()
|
||||
+ _albumPosition
|
||||
+ QPoint(addLeft, 0);
|
||||
return { realTopLeft, current.size() };
|
||||
}
|
||||
|
||||
QRect AlbumThumbnail::countCurrentGeometry(float64 progress) const {
|
||||
const auto now = countRealGeometry();
|
||||
if (_animateFromGeometry && progress < 1.) {
|
||||
return {
|
||||
anim::interpolate(_animateFromGeometry->x(), now.x(), progress),
|
||||
anim::interpolate(_animateFromGeometry->y(), now.y(), progress),
|
||||
anim::interpolate(_animateFromGeometry->width(), now.width(), progress),
|
||||
anim::interpolate(_animateFromGeometry->height(), now.height(), progress)
|
||||
};
|
||||
}
|
||||
return now;
|
||||
}
|
||||
|
||||
void AlbumThumbnail::finishAnimations() {
|
||||
_suggestedMoveAnimation.stop();
|
||||
}
|
||||
|
||||
QRect AlbumThumbnail::paintButtons(
|
||||
QPainter &p,
|
||||
QRect geometry,
|
||||
float64 shrinkProgress) {
|
||||
const auto &skipRight = st::sendBoxAlbumGroupSkipRight;
|
||||
const auto &skipTop = st::sendBoxAlbumGroupSkipTop;
|
||||
const auto outerWidth = geometry.width();
|
||||
const auto outerHeight = geometry.height();
|
||||
if (st::sendBoxAlbumGroupSize.width() <= outerWidth) {
|
||||
_buttons.setVertical(false);
|
||||
} else if (st::sendBoxAlbumGroupSize.height() <= outerHeight) {
|
||||
_buttons.setVertical(true);
|
||||
} else {
|
||||
// If the size is tiny, skip the buttons.
|
||||
return QRect();
|
||||
}
|
||||
const auto groupWidth = _buttons.width();
|
||||
const auto groupHeight = _buttons.height();
|
||||
|
||||
// If the width is too small,
|
||||
// it would be better to display the buttons in the center.
|
||||
const auto groupX = geometry.x() + ((groupWidth + skipRight * 2 > outerWidth)
|
||||
? (outerWidth - groupWidth) / 2
|
||||
: outerWidth - skipRight - groupWidth);
|
||||
const auto groupY = geometry.y() + ((groupHeight + skipTop * 2 > outerHeight)
|
||||
? (outerHeight - groupHeight) / 2
|
||||
: skipTop);
|
||||
|
||||
const auto opacity = p.opacity();
|
||||
p.setOpacity(1.0 - shrinkProgress);
|
||||
_buttons.paint(p, groupX, groupY);
|
||||
p.setOpacity(opacity);
|
||||
|
||||
return QRect(groupX, groupY, groupWidth, _buttons.height());
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
124
Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h
Normal file
124
Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/chat/attach/attach_controls.h"
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/grouped_layout.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace style {
|
||||
struct ComposeControls;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct PreparedFile;
|
||||
class IconButton;
|
||||
class SpoilerAnimation;
|
||||
|
||||
class AlbumThumbnail final {
|
||||
public:
|
||||
AlbumThumbnail(
|
||||
const style::ComposeControls &st,
|
||||
const PreparedFile &file,
|
||||
const GroupMediaLayout &layout,
|
||||
QWidget *parent,
|
||||
Fn<void()> repaint,
|
||||
Fn<void()> editCallback,
|
||||
Fn<void()> deleteCallback);
|
||||
|
||||
void moveToLayout(const GroupMediaLayout &layout);
|
||||
void animateLayoutToInitial();
|
||||
void resetLayoutAnimation();
|
||||
|
||||
void setSpoiler(bool spoiler);
|
||||
[[nodiscard]] bool hasSpoiler() const;
|
||||
|
||||
[[nodiscard]] int photoHeight() const;
|
||||
[[nodiscard]] int fileHeight() const;
|
||||
|
||||
void paintInAlbum(
|
||||
QPainter &p,
|
||||
int left,
|
||||
int top,
|
||||
float64 shrinkProgress,
|
||||
float64 moveProgress);
|
||||
void paintPhoto(Painter &p, int left, int top, int outerWidth);
|
||||
void paintFile(Painter &p, int left, int top, int outerWidth);
|
||||
|
||||
[[nodiscard]] QRect geometry() const;
|
||||
[[nodiscard]] bool containsPoint(QPoint position) const;
|
||||
[[nodiscard]] bool buttonsContainPoint(QPoint position) const;
|
||||
[[nodiscard]] AttachButtonType buttonTypeFromPoint(
|
||||
QPoint position) const;
|
||||
[[nodiscard]] int distanceTo(QPoint position) const;
|
||||
[[nodiscard]] bool isPointAfter(QPoint position) const;
|
||||
void moveInAlbum(QPoint to);
|
||||
[[nodiscard]] QPoint center() const;
|
||||
void suggestMove(float64 delta, Fn<void()> callback);
|
||||
void finishAnimations();
|
||||
|
||||
void setButtonVisible(bool value);
|
||||
void moveButtons(int thumbTop);
|
||||
|
||||
[[nodiscard]] bool isCompressedSticker() const;
|
||||
|
||||
static constexpr auto kShrinkDuration = crl::time(150);
|
||||
|
||||
private:
|
||||
QRect countRealGeometry() const;
|
||||
QRect countCurrentGeometry(float64 progress) const;
|
||||
void prepareCache(QSize size, int shrink);
|
||||
void drawSimpleFrame(QPainter &p, QRect to, QSize size) const;
|
||||
QRect paintButtons(
|
||||
QPainter &p,
|
||||
QRect geometry,
|
||||
float64 shrinkProgress);
|
||||
void paintPlayVideo(QPainter &p, QRect geometry);
|
||||
|
||||
const style::ComposeControls &_st;
|
||||
GroupMediaLayout _layout;
|
||||
std::optional<QRect> _animateFromGeometry;
|
||||
const QImage _fullPreview;
|
||||
const int _shrinkSize;
|
||||
const bool _isPhoto;
|
||||
const bool _isVideo;
|
||||
QPixmap _albumImage;
|
||||
QPixmap _albumImageBlurred;
|
||||
QImage _albumCache;
|
||||
QPoint _albumPosition;
|
||||
RectParts _albumCorners = RectPart::None;
|
||||
QPixmap _photo;
|
||||
QPixmap _photoBlurred;
|
||||
QPixmap _fileThumb;
|
||||
QString _name;
|
||||
QString _status;
|
||||
int _nameWidth = 0;
|
||||
int _statusWidth = 0;
|
||||
float64 _suggestedMove = 0.;
|
||||
Animations::Simple _suggestedMoveAnimation;
|
||||
int _lastShrinkValue = 0;
|
||||
AttachControls _buttons;
|
||||
|
||||
bool _isCompressedSticker = false;
|
||||
std::unique_ptr<SpoilerAnimation> _spoiler;
|
||||
QImage _cornerCache;
|
||||
Fn<void()> _repaint;
|
||||
|
||||
QRect _lastRectOfModify;
|
||||
QRect _lastRectOfButtons;
|
||||
|
||||
object_ptr<IconButton> _editMedia = { nullptr };
|
||||
object_ptr<IconButton> _deleteMedia = { nullptr };
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp
Normal file
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_bot_downloads.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
namespace Ui::BotWebView {
|
||||
namespace {
|
||||
|
||||
class Action final : public Menu::ItemBase {
|
||||
public:
|
||||
Action(
|
||||
not_null<RpWidget*> parent,
|
||||
const DownloadsEntry &entry,
|
||||
Fn<void(DownloadsAction)> callback);
|
||||
|
||||
bool isEnabled() const override;
|
||||
not_null<QAction*> action() const override { return _dummyAction; }
|
||||
void handleKeyPress(not_null<QKeyEvent*> e) override;
|
||||
|
||||
void refresh(const DownloadsEntry &entry);
|
||||
|
||||
private:
|
||||
QPoint prepareRippleStartPosition() const override {
|
||||
return mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
QImage prepareRippleMask() const override {
|
||||
return Ui::RippleAnimation::RectMask(size());
|
||||
}
|
||||
int contentHeight() const override { return _height; }
|
||||
|
||||
void prepare();
|
||||
void paint(Painter &p);
|
||||
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st = st::defaultMenu;
|
||||
|
||||
DownloadsEntry _entry;
|
||||
Text::String _name;
|
||||
FlatLabel _progress;
|
||||
IconButton _cancel;
|
||||
int _textWidth = 0;
|
||||
const int _height;
|
||||
};
|
||||
|
||||
Action::Action(
|
||||
not_null<RpWidget*> parent,
|
||||
const DownloadsEntry &entry,
|
||||
Fn<void(DownloadsAction)> callback)
|
||||
: ItemBase(parent, st::defaultMenu)
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _progress(this, st::botDownloadProgress)
|
||||
, _cancel(this, st::botDownloadCancel)
|
||||
, _height(st::ttlItemPadding.top()
|
||||
+ _st.itemStyle.font->height
|
||||
+ st::ttlItemTimerFont->height
|
||||
+ st::ttlItemPadding.bottom()) {
|
||||
setAcceptBoth(true);
|
||||
initResizeHook(parent->sizeValue());
|
||||
setClickedCallback([=] {
|
||||
if (isEnabled()) {
|
||||
callback(DownloadsAction::Open);
|
||||
}
|
||||
});
|
||||
_cancel.setClickedCallback([=] {
|
||||
callback(DownloadsAction::Cancel);
|
||||
});
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
Painter p(this);
|
||||
paint(p);
|
||||
}, lifetime());
|
||||
|
||||
widthValue() | rpl::on_next([=](int width) {
|
||||
_progress.moveToLeft(
|
||||
_st.itemPadding.left(),
|
||||
st::ttlItemPadding.top() + _st.itemStyle.font->height,
|
||||
width);
|
||||
|
||||
_cancel.moveToRight(
|
||||
_st.itemPadding.right(),
|
||||
(_height - _cancel.height()) / 2,
|
||||
width);
|
||||
}, lifetime());
|
||||
|
||||
_progress.setClickHandlerFilter([=](const auto &...) {
|
||||
callback(DownloadsAction::Retry);
|
||||
return false;
|
||||
});
|
||||
|
||||
enableMouseSelecting();
|
||||
refresh(entry);
|
||||
}
|
||||
|
||||
void Action::paint(Painter &p) {
|
||||
const auto selected = isSelected();
|
||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||
p.fillRect(0, 0, width(), _height, _st.itemBg);
|
||||
}
|
||||
p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);
|
||||
if (isEnabled()) {
|
||||
paintRipple(p, 0, 0);
|
||||
}
|
||||
|
||||
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
|
||||
_name.drawLeftElided(
|
||||
p,
|
||||
_st.itemPadding.left(),
|
||||
st::ttlItemPadding.top(),
|
||||
_textWidth,
|
||||
width());
|
||||
|
||||
_progress.setTextColorOverride(
|
||||
selected ? _st.itemFgShortcutOver->c : _st.itemFgShortcut->c);
|
||||
}
|
||||
|
||||
void Action::prepare() {
|
||||
const auto filenameWidth = _name.maxWidth();
|
||||
const auto progressWidth = _progress.textMaxWidth();
|
||||
const auto &padding = _st.itemPadding;
|
||||
|
||||
const auto goodWidth = std::max(filenameWidth, progressWidth);
|
||||
|
||||
// Example max width: "4000 / 4000 MB"
|
||||
const auto countWidth = [&](const QString &text) {
|
||||
return st::ttlItemTimerFont->width(text);
|
||||
};
|
||||
const auto maxProgressWidth = countWidth(tr::lng_media_save_progress(
|
||||
tr::now,
|
||||
lt_ready,
|
||||
"0000",
|
||||
lt_total,
|
||||
"0000",
|
||||
lt_mb,
|
||||
"MB"));
|
||||
const auto maxStartingWidth = countWidth(
|
||||
tr::lng_bot_download_starting(tr::now));
|
||||
const auto maxFailedWidth = countWidth(tr::lng_bot_download_failed(
|
||||
tr::now,
|
||||
lt_retry,
|
||||
tr::lng_bot_download_retry(tr::now)));
|
||||
|
||||
const auto cancel = _cancel.width() + padding.right();
|
||||
const auto paddings = padding.left() + padding.right() + cancel;
|
||||
const auto w = std::clamp(
|
||||
paddings + std::max({
|
||||
goodWidth,
|
||||
maxProgressWidth,
|
||||
maxStartingWidth,
|
||||
maxFailedWidth,
|
||||
}),
|
||||
_st.widthMin,
|
||||
_st.widthMax);
|
||||
_textWidth = w - paddings;
|
||||
_progress.resizeToWidth(_textWidth);
|
||||
setMinWidth(w);
|
||||
update();
|
||||
}
|
||||
|
||||
bool Action::isEnabled() const {
|
||||
return _entry.total > 0 && _entry.ready == _entry.total;
|
||||
}
|
||||
|
||||
void Action::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
if (!isSelected()) {
|
||||
return;
|
||||
}
|
||||
const auto key = e->key();
|
||||
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
||||
setClicked(Menu::TriggeredSource::Keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
void Action::refresh(const DownloadsEntry &entry) {
|
||||
_entry = entry;
|
||||
const auto filename = entry.path.split('/').last();
|
||||
_name.setMarkedText(_st.itemStyle, { filename }, kDefaultTextOptions);
|
||||
|
||||
const auto progressText = (entry.total && entry.total == entry.ready)
|
||||
? TextWithEntities{ FormatSizeText(entry.total) }
|
||||
: entry.loading
|
||||
? (entry.total
|
||||
? TextWithEntities{
|
||||
FormatProgressText(entry.ready, entry.total),
|
||||
}
|
||||
: tr::lng_bot_download_starting(tr::now, tr::marked))
|
||||
: tr::lng_bot_download_failed(
|
||||
tr::now,
|
||||
lt_retry,
|
||||
Text::Link(tr::lng_bot_download_retry(tr::now)),
|
||||
tr::marked);
|
||||
_progress.setMarkedText(progressText);
|
||||
|
||||
const auto enabled = isEnabled();
|
||||
setCursor(enabled ? style::cur_pointer : style::cur_default);
|
||||
_cancel.setVisible(!enabled && _entry.loading);
|
||||
_progress.setAttribute(Qt::WA_TransparentForMouseEvents, enabled);
|
||||
|
||||
prepare();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FnMut<void(not_null<PopupMenu*>)> FillAttachBotDownloadsSubmenu(
|
||||
rpl::producer<std::vector<DownloadsEntry>> content,
|
||||
Fn<void(uint32, DownloadsAction)> callback) {
|
||||
return [callback, moved = std::move(content)](
|
||||
not_null<PopupMenu*> menu) mutable {
|
||||
struct Row {
|
||||
not_null<Action*> action;
|
||||
uint32 id = 0;
|
||||
};
|
||||
struct State {
|
||||
std::vector<Row> rows;
|
||||
};
|
||||
const auto state = menu->lifetime().make_state<State>();
|
||||
std::move(
|
||||
moved
|
||||
) | rpl::on_next([=](
|
||||
const std::vector<DownloadsEntry> &entries) {
|
||||
auto found = base::flat_set<uint32>();
|
||||
for (const auto &entry : entries | ranges::views::reverse) {
|
||||
const auto id = entry.id;
|
||||
const auto path = entry.path;
|
||||
const auto i = ranges::find(state->rows, id, &Row::id);
|
||||
found.emplace(id);
|
||||
|
||||
if (i != end(state->rows)) {
|
||||
i->action->refresh(entry);
|
||||
} else {
|
||||
auto action = base::make_unique_q<Action>(
|
||||
menu,
|
||||
entry,
|
||||
[=](DownloadsAction type) { callback(id, type); });
|
||||
state->rows.push_back({
|
||||
.action = action.get(),
|
||||
.id = id,
|
||||
});
|
||||
menu->addAction(std::move(action));
|
||||
}
|
||||
}
|
||||
for (auto i = begin(state->rows); i != end(state->rows);) {
|
||||
if (!found.contains(i->id)) {
|
||||
menu->removeAction(i - begin(state->rows));
|
||||
i = state->rows.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}, menu->lifetime());
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Ui::BotWebView
|
||||
47
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h
Normal file
47
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::BotWebView {
|
||||
|
||||
struct DownloadsProgress {
|
||||
uint64 ready = 0;
|
||||
uint64 total : 63 = 0;
|
||||
uint64 loading : 1 = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const DownloadsProgress &a,
|
||||
const DownloadsProgress &b) = default;
|
||||
};
|
||||
|
||||
struct DownloadsEntry {
|
||||
uint32 id = 0;
|
||||
QString url;
|
||||
QString path;
|
||||
uint64 ready : 63 = 0;
|
||||
uint64 loading : 1 = 0;
|
||||
uint64 total : 63 = 0;
|
||||
uint64 failed : 1 = 0;
|
||||
};
|
||||
|
||||
enum class DownloadsAction {
|
||||
Open,
|
||||
Retry,
|
||||
Cancel,
|
||||
};
|
||||
|
||||
[[nodiscard]] auto FillAttachBotDownloadsSubmenu(
|
||||
rpl::producer<std::vector<DownloadsEntry>> content,
|
||||
Fn<void(uint32, DownloadsAction)> callback)
|
||||
-> FnMut<void(not_null<PopupMenu*>)>;
|
||||
|
||||
} // namespace Ui::BotWebView
|
||||
2150
Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
Normal file
2150
Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
Normal file
File diff suppressed because it is too large
Load Diff
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
Normal file
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/expected.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/flags.h"
|
||||
#include "ui/rect_part.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "webview/webview_common.h"
|
||||
|
||||
class QJsonObject;
|
||||
class QJsonValue;
|
||||
|
||||
namespace Ui {
|
||||
class FlatLabel;
|
||||
class BoxContent;
|
||||
class RpWidget;
|
||||
class SeparatePanel;
|
||||
class IconButton;
|
||||
enum class LayerOption;
|
||||
using LayerOptions = base::flags<LayerOption>;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Webview {
|
||||
struct Available;
|
||||
} // namespace Webview
|
||||
|
||||
namespace Ui::BotWebView {
|
||||
|
||||
struct DownloadsProgress;
|
||||
struct DownloadsEntry;
|
||||
enum class DownloadsAction;
|
||||
|
||||
[[nodiscard]] TextWithEntities ErrorText(const Webview::Available &info);
|
||||
|
||||
enum class MenuButton {
|
||||
None = 0x00,
|
||||
OpenBot = 0x01,
|
||||
RemoveFromMenu = 0x02,
|
||||
RemoveFromMainMenu = 0x04,
|
||||
ShareGame = 0x08,
|
||||
};
|
||||
inline constexpr bool is_flag_type(MenuButton) { return true; }
|
||||
using MenuButtons = base::flags<MenuButton>;
|
||||
|
||||
using CustomMethodResult = base::expected<QByteArray, QString>;
|
||||
struct CustomMethodRequest {
|
||||
QString method;
|
||||
QByteArray params;
|
||||
Fn<void(CustomMethodResult)> callback;
|
||||
};
|
||||
|
||||
struct SetEmojiStatusRequest {
|
||||
uint64 customEmojiId = 0;
|
||||
TimeId duration = 0;
|
||||
Fn<void(QString)> callback;
|
||||
};
|
||||
|
||||
struct DownloadFileRequest {
|
||||
QString url;
|
||||
QString name;
|
||||
Fn<void(bool)> callback;
|
||||
};
|
||||
|
||||
struct SendPreparedMessageRequest {
|
||||
QString id = 0;
|
||||
Fn<void(QString)> callback;
|
||||
};
|
||||
|
||||
class Delegate {
|
||||
public:
|
||||
[[nodiscard]] virtual Webview::ThemeParams botThemeParams() = 0;
|
||||
[[nodiscard]] virtual auto botDownloads(bool forceCheck = false)
|
||||
-> const std::vector<DownloadsEntry> & = 0;
|
||||
virtual void botDownloadsAction(uint32 id, DownloadsAction type) = 0;
|
||||
virtual bool botHandleLocalUri(QString uri, bool keepOpen) = 0;
|
||||
virtual void botHandleInvoice(QString slug) = 0;
|
||||
virtual void botHandleMenuButton(MenuButton button) = 0;
|
||||
virtual bool botValidateExternalLink(QString uri) = 0;
|
||||
virtual void botOpenIvLink(QString uri) = 0;
|
||||
virtual void botSendData(QByteArray data) = 0;
|
||||
virtual void botSwitchInlineQuery(
|
||||
std::vector<QString> chatTypes,
|
||||
QString query) = 0;
|
||||
virtual void botCheckWriteAccess(Fn<void(bool allowed)> callback) = 0;
|
||||
virtual void botAllowWriteAccess(Fn<void(bool allowed)> callback) = 0;
|
||||
virtual bool botStorageWrite(
|
||||
QString key,
|
||||
std::optional<QString> value) = 0;
|
||||
[[nodiscard]] virtual std::optional<QString> botStorageRead(
|
||||
QString key) = 0;
|
||||
virtual void botStorageClear() = 0;
|
||||
virtual void botRequestEmojiStatusAccess(
|
||||
Fn<void(bool allowed)> callback) = 0;
|
||||
virtual void botSharePhone(Fn<void(bool shared)> callback) = 0;
|
||||
virtual void botInvokeCustomMethod(CustomMethodRequest request) = 0;
|
||||
virtual void botSetEmojiStatus(SetEmojiStatusRequest request) = 0;
|
||||
virtual void botDownloadFile(DownloadFileRequest request) = 0;
|
||||
virtual void botSendPreparedMessage(
|
||||
SendPreparedMessageRequest request) = 0;
|
||||
virtual void botVerifyAge(int age) = 0;
|
||||
virtual void botOpenPrivacyPolicy() = 0;
|
||||
virtual void botClose() = 0;
|
||||
};
|
||||
|
||||
struct Args {
|
||||
QString url;
|
||||
Webview::StorageId storageId;
|
||||
rpl::producer<QString> title;
|
||||
object_ptr<Ui::RpWidget> titleBadge = { nullptr };
|
||||
rpl::producer<QString> bottom;
|
||||
not_null<Delegate*> delegate;
|
||||
MenuButtons menuButtons;
|
||||
bool fullscreen = false;
|
||||
bool allowClipboardRead = false;
|
||||
rpl::producer<DownloadsProgress> downloadsProgress;
|
||||
};
|
||||
|
||||
class Panel final : public base::has_weak_ptr {
|
||||
public:
|
||||
explicit Panel(Args &&args);
|
||||
~Panel();
|
||||
|
||||
void requestActivate();
|
||||
void toggleProgress(bool shown);
|
||||
|
||||
void showBox(object_ptr<BoxContent> box);
|
||||
void showBox(
|
||||
object_ptr<BoxContent> box,
|
||||
LayerOptions options,
|
||||
anim::type animated);
|
||||
void hideLayer(anim::type animated);
|
||||
void showToast(TextWithEntities &&text);
|
||||
not_null<QWidget*> toastParent() const;
|
||||
void showCriticalError(const TextWithEntities &text);
|
||||
void showWebviewError(
|
||||
const QString &text,
|
||||
const Webview::Available &information);
|
||||
|
||||
void updateThemeParams(const Webview::ThemeParams ¶ms);
|
||||
|
||||
void hideForPayment();
|
||||
void invoiceClosed(const QString &slug, const QString &status);
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
class Button;
|
||||
struct Progress;
|
||||
struct WebviewWithLifetime;
|
||||
|
||||
bool showWebview(Args &&args, const Webview::ThemeParams ¶ms);
|
||||
|
||||
bool createWebview(const Webview::ThemeParams ¶ms);
|
||||
void createWebviewBottom();
|
||||
void showWebviewProgress();
|
||||
void hideWebviewProgress();
|
||||
void setupDownloadsProgress(
|
||||
not_null<RpWidget*> button,
|
||||
rpl::producer<DownloadsProgress> progress,
|
||||
bool fullscreen);
|
||||
void setTitle(rpl::producer<QString> title);
|
||||
void sendDataMessage(const QJsonObject &args);
|
||||
void switchInlineQueryMessage(const QJsonObject &args);
|
||||
void processSendMessageRequest(const QJsonObject &args);
|
||||
void processEmojiStatusRequest(const QJsonObject &args);
|
||||
void processEmojiStatusAccessRequest();
|
||||
void processStorageSaveKey(const QJsonObject &args);
|
||||
void processStorageGetKey(const QJsonObject &args);
|
||||
void processStorageClear(const QJsonObject &args);
|
||||
void processButtonMessage(
|
||||
std::unique_ptr<Button> &button,
|
||||
const QJsonObject &args);
|
||||
void processBackButtonMessage(const QJsonObject &args);
|
||||
void processSettingsButtonMessage(const QJsonObject &args);
|
||||
void processHeaderColor(const QJsonObject &args);
|
||||
void processBackgroundColor(const QJsonObject &args);
|
||||
void processBottomBarColor(const QJsonObject &args);
|
||||
void processDownloadRequest(const QJsonObject &args);
|
||||
void openTgLink(const QJsonObject &args);
|
||||
void openExternalLink(const QJsonObject &args);
|
||||
void openInvoice(const QJsonObject &args);
|
||||
void openPopup(const QJsonObject &args);
|
||||
void openScanQrPopup(const QJsonObject &args);
|
||||
void openShareStory(const QJsonObject &args);
|
||||
void requestWriteAccess();
|
||||
void replyRequestWriteAccess(bool allowed);
|
||||
void requestPhone();
|
||||
void replyRequestPhone(bool shared);
|
||||
void invokeCustomMethod(const QJsonObject &args);
|
||||
void replyCustomMethod(QJsonValue requestId, QJsonObject response);
|
||||
void requestClipboardText(const QJsonObject &args);
|
||||
void setupClosingBehaviour(const QJsonObject &args);
|
||||
void replyDeviceStorage(
|
||||
const QJsonObject &args,
|
||||
const QString &event,
|
||||
QJsonObject response);
|
||||
void deviceStorageFailed(const QJsonObject &args, QString error);
|
||||
void secureStorageFailed(const QJsonObject &args);
|
||||
void createButton(std::unique_ptr<Button> &button);
|
||||
void scheduleCloseWithConfirmation();
|
||||
void closeWithConfirmation();
|
||||
void sendViewport();
|
||||
void sendSafeArea();
|
||||
void sendContentSafeArea();
|
||||
void sendFullScreen();
|
||||
|
||||
void updateColorOverrides(const Webview::ThemeParams ¶ms);
|
||||
void overrideBodyColor(std::optional<QColor> color);
|
||||
|
||||
using EventData = std::variant<QString, QJsonObject>;
|
||||
void postEvent(const QString &event);
|
||||
void postEvent(const QString &event, EventData data);
|
||||
|
||||
[[nodiscard]] bool allowOpenLink() const;
|
||||
[[nodiscard]] bool allowClipboardQuery() const;
|
||||
[[nodiscard]] bool progressWithBackground() const;
|
||||
[[nodiscard]] QRect progressRect() const;
|
||||
void setupProgressGeometry();
|
||||
void layoutButtons();
|
||||
|
||||
Webview::StorageId _storageId;
|
||||
const not_null<Delegate*> _delegate;
|
||||
bool _closeNeedConfirmation = false;
|
||||
bool _hasSettingsButton = false;
|
||||
MenuButtons _menuButtons = {};
|
||||
std::unique_ptr<SeparatePanel> _widget;
|
||||
std::unique_ptr<WebviewWithLifetime> _webview;
|
||||
std::unique_ptr<RpWidget> _webviewBottom;
|
||||
QPointer<FlatLabel> _webviewBottomLabel;
|
||||
rpl::variable<QString> _bottomText;
|
||||
QPointer<RpWidget> _webviewParent;
|
||||
std::unique_ptr<RpWidget> _bottomButtonsBg;
|
||||
std::unique_ptr<Button> _mainButton;
|
||||
std::unique_ptr<Button> _secondaryButton;
|
||||
RectPart _secondaryPosition = RectPart::Left;
|
||||
rpl::variable<int> _footerHeight = 0;
|
||||
std::unique_ptr<Progress> _progress;
|
||||
rpl::event_stream<> _themeUpdateForced;
|
||||
std::optional<QColor> _bottomBarColor;
|
||||
rpl::lifetime _headerColorLifetime;
|
||||
rpl::lifetime _bodyColorLifetime;
|
||||
rpl::lifetime _bottomBarColorLifetime;
|
||||
rpl::event_stream<> _downloadsUpdated;
|
||||
rpl::variable<bool> _fullscreen = false;
|
||||
bool _layerShown : 1 = false;
|
||||
bool _webviewProgress : 1 = false;
|
||||
bool _themeUpdateScheduled : 1 = false;
|
||||
bool _hiddenForPayment : 1 = false;
|
||||
bool _closeWithConfirmationScheduled : 1 = false;
|
||||
bool _allowClipboardRead : 1 = false;
|
||||
bool _inBlockingRequest : 1 = false;
|
||||
bool _headerColorReceived : 1 = false;
|
||||
bool _bodyColorReceived : 1 = false;
|
||||
bool _bottomColorReceived : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] std::unique_ptr<Panel> Show(Args &&args);
|
||||
|
||||
} // namespace Ui::BotWebView
|
||||
117
Telegram/SourceFiles/ui/chat/attach/attach_controls.cpp
Normal file
117
Telegram/SourceFiles/ui/chat/attach/attach_controls.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_controls.h"
|
||||
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
AttachControls::AttachControls()
|
||||
: _rect(st::sendBoxAlbumGroupRadius, st::roundedBg) {
|
||||
}
|
||||
|
||||
void AttachControls::paint(QPainter &p, int x, int y) {
|
||||
const auto groupWidth = width();
|
||||
const auto groupHeight = height();
|
||||
const auto full = (_type == Type::Full);
|
||||
|
||||
QRect groupRect(x, y, groupWidth, groupHeight);
|
||||
_rect.paint(p, groupRect);
|
||||
|
||||
if (full) {
|
||||
const auto groupHalfWidth = groupWidth / 2;
|
||||
const auto groupHalfHeight = groupHeight / 2;
|
||||
const auto editRect = _vertical
|
||||
? QRect(x, y, groupWidth, groupHalfHeight)
|
||||
: QRect(x, y, groupHalfWidth, groupHeight);
|
||||
st::sendBoxAlbumGroupButtonMediaEdit.paintInCenter(p, editRect);
|
||||
const auto deleteRect = _vertical
|
||||
? QRect(x, y + groupHalfHeight, groupWidth, groupHalfHeight)
|
||||
: QRect(x + groupHalfWidth, y, groupHalfWidth, groupHeight);
|
||||
st::sendBoxAlbumGroupButtonMediaDelete.paintInCenter(p, deleteRect);
|
||||
} else if (_type == Type::EditOnly) {
|
||||
st::sendBoxAlbumButtonMediaEdit.paintInCenter(p, groupRect);
|
||||
}
|
||||
}
|
||||
|
||||
int AttachControls::width() const {
|
||||
return (_type == Type::Full)
|
||||
? (_vertical
|
||||
? st::sendBoxAlbumGroupSizeVertical.width()
|
||||
: st::sendBoxAlbumGroupSize.width())
|
||||
: (_type == Type::EditOnly)
|
||||
? st::sendBoxAlbumSmallGroupSize.width()
|
||||
: 0;
|
||||
}
|
||||
|
||||
int AttachControls::height() const {
|
||||
return (_type == Type::Full)
|
||||
? (_vertical
|
||||
? st::sendBoxAlbumGroupSizeVertical.height()
|
||||
: st::sendBoxAlbumGroupSize.height())
|
||||
: (_type == Type::EditOnly)
|
||||
? st::sendBoxAlbumSmallGroupSize.height()
|
||||
: 0;
|
||||
}
|
||||
|
||||
AttachControls::Type AttachControls::type() const {
|
||||
return _type;
|
||||
}
|
||||
|
||||
bool AttachControls::vertical() const {
|
||||
return _vertical;
|
||||
}
|
||||
|
||||
void AttachControls::setType(Type type) {
|
||||
if (_type != type) {
|
||||
_type = type;
|
||||
}
|
||||
}
|
||||
|
||||
void AttachControls::setVertical(bool vertical) {
|
||||
_vertical = vertical;
|
||||
}
|
||||
|
||||
AttachControlsWidget::AttachControlsWidget(
|
||||
not_null<RpWidget*> parent,
|
||||
AttachControls::Type type)
|
||||
: RpWidget(parent)
|
||||
, _edit(base::make_unique_q<AbstractButton>(this))
|
||||
, _delete(base::make_unique_q<AbstractButton>(this)) {
|
||||
_controls.setType(type);
|
||||
|
||||
const auto w = _controls.width();
|
||||
resize(w, _controls.height());
|
||||
|
||||
if (type == AttachControls::Type::Full) {
|
||||
_edit->resize(w / 2, _controls.height());
|
||||
_delete->resize(w / 2, _controls.height());
|
||||
|
||||
_edit->moveToLeft(0, 0, w);
|
||||
_delete->moveToRight(0, 0, w);
|
||||
} else if (type == AttachControls::Type::EditOnly) {
|
||||
_edit->resize(w, _controls.height());
|
||||
_edit->moveToLeft(0, 0, w);
|
||||
}
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(this);
|
||||
_controls.paint(p, 0, 0);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
rpl::producer<> AttachControlsWidget::editRequests() const {
|
||||
return _edit->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
rpl::producer<> AttachControlsWidget::deleteRequests() const {
|
||||
return _delete->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
58
Telegram/SourceFiles/ui/chat/attach/attach_controls.h
Normal file
58
Telegram/SourceFiles/ui/chat/attach/attach_controls.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class AttachControls final {
|
||||
public:
|
||||
enum class Type {
|
||||
Full,
|
||||
EditOnly,
|
||||
None,
|
||||
};
|
||||
|
||||
AttachControls();
|
||||
|
||||
void paint(QPainter &p, int x, int y);
|
||||
void setType(Type type);
|
||||
void setVertical(bool vertical);
|
||||
|
||||
[[nodiscard]] int width() const;
|
||||
[[nodiscard]] int height() const;
|
||||
[[nodiscard]] Type type() const;
|
||||
[[nodiscard]] bool vertical() const;
|
||||
|
||||
private:
|
||||
RoundRect _rect;
|
||||
Type _type = Type::Full;
|
||||
bool _vertical = false;
|
||||
|
||||
};
|
||||
|
||||
class AttachControlsWidget final : public RpWidget {
|
||||
public:
|
||||
AttachControlsWidget(
|
||||
not_null<RpWidget*> parent,
|
||||
AttachControls::Type type = AttachControls::Type::Full);
|
||||
|
||||
[[nodiscard]] rpl::producer<> editRequests() const;
|
||||
[[nodiscard]] rpl::producer<> deleteRequests() const;
|
||||
|
||||
private:
|
||||
const base::unique_qptr<AbstractButton> _edit;
|
||||
const base::unique_qptr<AbstractButton> _delete;
|
||||
AttachControls _controls;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
30
Telegram/SourceFiles/ui/chat/attach/attach_extensions.cpp
Normal file
30
Telegram/SourceFiles/ui/chat/attach/attach_extensions.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_extensions.h"
|
||||
|
||||
#include <QtCore/QMimeDatabase>
|
||||
#include <QtGui/QImageReader>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
const QStringList &ImageExtensions() {
|
||||
static const auto result = [] {
|
||||
const auto formats = QImageReader::supportedImageFormats();
|
||||
return formats | ranges::views::transform([](const auto &format) {
|
||||
return '.' + format.toLower();
|
||||
}) | ranges::views::filter([](const auto &format) {
|
||||
const auto mimes = QMimeDatabase().mimeTypesForFileName(
|
||||
u"test"_q + format);
|
||||
return !mimes.isEmpty()
|
||||
&& mimes.front().name().startsWith(u"image/"_q);
|
||||
}) | ranges::to<QStringList>;
|
||||
}();
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
14
Telegram/SourceFiles/ui/chat/attach/attach_extensions.h
Normal file
14
Telegram/SourceFiles/ui/chat/attach/attach_extensions.h
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
|
||||
[[nodiscard]] const QStringList &ImageExtensions();
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_item_single_file_preview.h"
|
||||
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/view/media/history_view_document.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/text/format_song_name.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
AttachControls::Type CheckControlsType(
|
||||
not_null<HistoryItem*> item,
|
||||
AttachControls::Type type) {
|
||||
const auto media = item->media();
|
||||
Assert(media != nullptr);
|
||||
return media->allowsEditMedia()
|
||||
? type
|
||||
: AttachControls::Type::None;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ItemSingleFilePreview::ItemSingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
not_null<HistoryItem*> item,
|
||||
AttachControls::Type type)
|
||||
: AbstractSingleFilePreview(parent, st, CheckControlsType(item, type)) {
|
||||
const auto media = item->media();
|
||||
Assert(media != nullptr);
|
||||
const auto document = media->document();
|
||||
Assert(document != nullptr);
|
||||
|
||||
_documentMedia = document->createMediaView();
|
||||
_documentMedia->thumbnailWanted(item->fullId());
|
||||
|
||||
rpl::single(rpl::empty) | rpl::then(
|
||||
document->session().downloaderTaskFinished()
|
||||
) | rpl::on_next([=] {
|
||||
if (_documentMedia->thumbnail()) {
|
||||
_lifetimeDownload.destroy();
|
||||
}
|
||||
preparePreview(document);
|
||||
}, _lifetimeDownload);
|
||||
}
|
||||
|
||||
void ItemSingleFilePreview::preparePreview(not_null<DocumentData*> document) {
|
||||
AbstractSingleFilePreview::Data data;
|
||||
|
||||
const auto preview = _documentMedia->thumbnail()
|
||||
? _documentMedia->thumbnail()->original()
|
||||
: QImage();
|
||||
|
||||
prepareThumbFor(data, preview);
|
||||
data.fileIsImage = document->isImage();
|
||||
data.fileIsAudio = document->isAudioFile() || document->isVoiceMessage();
|
||||
|
||||
if (data.fileIsImage) {
|
||||
data.name = document->filename();
|
||||
// data.statusText = FormatImageSizeText(preview.size()
|
||||
// / preview.devicePixelRatio());
|
||||
} else if (data.fileIsAudio) {
|
||||
auto filename = document->filename();
|
||||
|
||||
auto songTitle = QString();
|
||||
auto songPerformer = QString();
|
||||
if (const auto song = document->song()) {
|
||||
songTitle = song->title;
|
||||
songPerformer = song->performer;
|
||||
|
||||
if (document->isSongWithCover()) {
|
||||
const auto size = QSize(
|
||||
st::attachPreviewLayout.thumbSize,
|
||||
st::attachPreviewLayout.thumbSize);
|
||||
auto thumb = QPixmap(size);
|
||||
thumb.fill(Qt::transparent);
|
||||
Painter p(&thumb);
|
||||
|
||||
HistoryView::DrawThumbnailAsSongCover(
|
||||
p,
|
||||
st::songCoverOverlayFg,
|
||||
_documentMedia,
|
||||
QRect(QPoint(), size));
|
||||
data.fileThumb = std::move(thumb);
|
||||
}
|
||||
} else if (document->isVoiceMessage()) {
|
||||
songTitle = tr::lng_media_audio(tr::now);
|
||||
}
|
||||
|
||||
data.name = Text::FormatSongName(filename, songTitle, songPerformer)
|
||||
.string();
|
||||
} else {
|
||||
data.name = document->filename();
|
||||
}
|
||||
data.statusText = FormatSizeText(document->size);
|
||||
|
||||
setData(data);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/chat/attach/attach_abstract_single_file_preview.h"
|
||||
|
||||
class HistoryItem;
|
||||
class DocumentData;
|
||||
|
||||
namespace Data {
|
||||
class DocumentMedia;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct PreparedFile;
|
||||
class IconButton;
|
||||
|
||||
class ItemSingleFilePreview final : public AbstractSingleFilePreview {
|
||||
public:
|
||||
ItemSingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
not_null<HistoryItem*> item,
|
||||
AttachControls::Type type);
|
||||
|
||||
private:
|
||||
void preparePreview(not_null<DocumentData*> document);
|
||||
|
||||
std::shared_ptr<::Data::DocumentMedia> _documentMedia;
|
||||
|
||||
rpl::lifetime _lifetimeDownload;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_item_single_media_preview.h"
|
||||
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_photo_media.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_streaming.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/view/media/history_view_document.h"
|
||||
#include "main/main_session.h"
|
||||
#include "media/streaming/media_streaming_document.h"
|
||||
#include "media/streaming/media_streaming_instance.h"
|
||||
#include "media/streaming/media_streaming_loader_local.h"
|
||||
#include "media/streaming/media_streaming_player.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
using namespace ::Media::Streaming;
|
||||
|
||||
} // namespace
|
||||
|
||||
ItemSingleMediaPreview::ItemSingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
not_null<HistoryItem*> item,
|
||||
AttachControls::Type type)
|
||||
: AbstractSingleMediaPreview(parent, st, type, [=](AttachActionType type) {
|
||||
if (type == AttachActionType::EditCover) {
|
||||
return _isVideoFile;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
, _gifPaused(std::move(gifPaused))
|
||||
, _isVideoFile(item->media()->document()
|
||||
&& item->media()->document()->isVideoFile())
|
||||
, _fullId(item->fullId()) {
|
||||
const auto media = item->media();
|
||||
Assert(media != nullptr);
|
||||
|
||||
Main::Session *session = nullptr;
|
||||
|
||||
if (const auto photo = media->photo()) {
|
||||
_photoMedia = photo->createMediaView();
|
||||
_photoMedia->wanted(Data::PhotoSize::Large, item->fullId());
|
||||
|
||||
session = &photo->session();
|
||||
} else if (const auto document = media->document()) {
|
||||
_documentMedia = document->createMediaView();
|
||||
_documentMedia->thumbnailWanted(item->fullId());
|
||||
|
||||
session = &document->session();
|
||||
if (document->isAnimation() || document->isVideoFile()) {
|
||||
setAnimated(true);
|
||||
prepareStreamedPreview();
|
||||
}
|
||||
} else {
|
||||
Unexpected("Photo or document should be set.");
|
||||
}
|
||||
|
||||
struct ThumbInfo {
|
||||
bool loaded = false;
|
||||
Image *image = nullptr;
|
||||
};
|
||||
|
||||
const auto computeThumbInfo = [=]() -> ThumbInfo {
|
||||
using Size = Data::PhotoSize;
|
||||
if (_documentMedia) {
|
||||
return { true, _documentMedia->thumbnail() };
|
||||
} else if (const auto large = _photoMedia->image(Size::Large)) {
|
||||
return { true, large };
|
||||
} else if (const auto thumbnail = _photoMedia->image(
|
||||
Size::Thumbnail)) {
|
||||
return { false, thumbnail };
|
||||
} else if (const auto small = _photoMedia->image(Size::Small)) {
|
||||
return { false, small };
|
||||
} else {
|
||||
return { false, _photoMedia->thumbnailInline() };
|
||||
}
|
||||
};
|
||||
|
||||
rpl::single(rpl::empty) | rpl::then(
|
||||
session->downloaderTaskFinished()
|
||||
) | rpl::on_next([=] {
|
||||
const auto computed = computeThumbInfo();
|
||||
if (!computed.image) {
|
||||
if (_documentMedia && !_documentMedia->owner()->hasThumbnail()) {
|
||||
const auto size = _documentMedia->owner()->dimensions.scaled(
|
||||
st::sendMediaPreviewSize,
|
||||
st::confirmMaxHeight,
|
||||
Qt::KeepAspectRatio);
|
||||
if (!size.isEmpty()) {
|
||||
auto empty = QImage(
|
||||
size,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
empty.fill(Qt::black);
|
||||
preparePreview(empty);
|
||||
}
|
||||
_lifetimeDownload.destroy();
|
||||
}
|
||||
return;
|
||||
} else if (computed.loaded) {
|
||||
_lifetimeDownload.destroy();
|
||||
}
|
||||
preparePreview(computed.image->original());
|
||||
}, _lifetimeDownload);
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::prepareStreamedPreview() {
|
||||
if (_streamed || !_documentMedia) {
|
||||
return;
|
||||
}
|
||||
const auto document = _documentMedia
|
||||
? _documentMedia->owner().get()
|
||||
: nullptr;
|
||||
if (document && document->isAnimation()) {
|
||||
setupStreamedPreview(
|
||||
document->owner().streaming().sharedDocument(
|
||||
document,
|
||||
_fullId));
|
||||
}
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::setupStreamedPreview(
|
||||
std::shared_ptr<Document> shared) {
|
||||
if (!shared) {
|
||||
return;
|
||||
}
|
||||
_streamed = std::make_unique<Instance>(
|
||||
std::move(shared),
|
||||
[=] { update(); });
|
||||
_streamed->lockPlayer();
|
||||
_streamed->player().updates(
|
||||
) | rpl::on_next_error([=](Update &&update) {
|
||||
handleStreamingUpdate(std::move(update));
|
||||
}, [=](Error &&error) {
|
||||
handleStreamingError(std::move(error));
|
||||
}, _streamed->lifetime());
|
||||
|
||||
if (_streamed->ready()) {
|
||||
streamingReady(base::duplicate(_streamed->info()));
|
||||
}
|
||||
checkStreamedIsStarted();
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::handleStreamingUpdate(Update &&update) {
|
||||
v::match(update.data, [&](Information &update) {
|
||||
streamingReady(std::move(update));
|
||||
}, [](PreloadedVideo) {
|
||||
}, [&](UpdateVideo) {
|
||||
this->update();
|
||||
}, [](PreloadedAudio) {
|
||||
}, [](UpdateAudio) {
|
||||
}, [](WaitingForData) {
|
||||
}, [](SpeedEstimate) {
|
||||
}, [](MutedByOther) {
|
||||
}, [](Finished) {
|
||||
});
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::handleStreamingError(Error &&error) {
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::streamingReady(Information &&info) {
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::checkStreamedIsStarted() {
|
||||
if (!_streamed) {
|
||||
return;
|
||||
} else if (_streamed->paused()) {
|
||||
_streamed->resume();
|
||||
}
|
||||
if (!_streamed->active() && !_streamed->failed()) {
|
||||
startStreamedPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::startStreamedPlayer() {
|
||||
auto options = ::Media::Streaming::PlaybackOptions();
|
||||
options.audioId = _documentMedia
|
||||
? AudioMsgId(_documentMedia->owner(), _fullId)
|
||||
: AudioMsgId();
|
||||
options.waitForMarkAsShown = true;
|
||||
//if (!_streamed->withSound) {
|
||||
options.mode = ::Media::Streaming::Mode::Video;
|
||||
options.loop = true;
|
||||
//}
|
||||
_streamed->play(options);
|
||||
}
|
||||
|
||||
bool ItemSingleMediaPreview::supportsSpoilers() const {
|
||||
return false; // We are not allowed to change existing spoiler setting.
|
||||
}
|
||||
|
||||
bool ItemSingleMediaPreview::drawBackground() const {
|
||||
return true; // A sticker can't be here.
|
||||
}
|
||||
|
||||
bool ItemSingleMediaPreview::tryPaintAnimation(QPainter &p) {
|
||||
checkStreamedIsStarted();
|
||||
if (_streamed
|
||||
&& _streamed->player().ready()
|
||||
&& !_streamed->player().videoSize().isEmpty()) {
|
||||
const auto s = QSize(previewWidth(), previewHeight());
|
||||
const auto paused = _gifPaused();
|
||||
|
||||
auto request = ::Media::Streaming::FrameRequest();
|
||||
request.outer = s * style::DevicePixelRatio();
|
||||
request.resize = s * style::DevicePixelRatio();
|
||||
p.drawImage(
|
||||
QRect(
|
||||
previewLeft(),
|
||||
previewTop(),
|
||||
previewWidth(),
|
||||
previewHeight()),
|
||||
_streamed->frame(request));
|
||||
if (!paused) {
|
||||
_streamed->markFrameShown();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ItemSingleMediaPreview::isAnimatedPreviewReady() const {
|
||||
return _streamed != nullptr;
|
||||
}
|
||||
|
||||
auto ItemSingleMediaPreview::sharedPhotoMedia() const
|
||||
-> std::shared_ptr<::Data::PhotoMedia> {
|
||||
return _photoMedia;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/chat/attach/attach_abstract_single_media_preview.h"
|
||||
|
||||
namespace Data {
|
||||
class DocumentMedia;
|
||||
class PhotoMedia;
|
||||
} // namespace Data
|
||||
|
||||
namespace Media {
|
||||
namespace Streaming {
|
||||
class Instance;
|
||||
class Document;
|
||||
struct Update;
|
||||
enum class Error;
|
||||
struct Information;
|
||||
} // namespace Streaming
|
||||
} // namespace Media
|
||||
|
||||
class HistoryItem;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class ItemSingleMediaPreview final : public AbstractSingleMediaPreview {
|
||||
public:
|
||||
ItemSingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
not_null<HistoryItem*> item,
|
||||
AttachControls::Type type);
|
||||
|
||||
std::shared_ptr<::Data::PhotoMedia> sharedPhotoMedia() const;
|
||||
|
||||
protected:
|
||||
bool supportsSpoilers() const override;
|
||||
bool drawBackground() const override;
|
||||
bool tryPaintAnimation(QPainter &p) override;
|
||||
bool isAnimatedPreviewReady() const override;
|
||||
|
||||
private:
|
||||
void prepareStreamedPreview();
|
||||
void checkStreamedIsStarted();
|
||||
void setupStreamedPreview(
|
||||
std::shared_ptr<::Media::Streaming::Document> shared);
|
||||
void handleStreamingUpdate(::Media::Streaming::Update &&update);
|
||||
void handleStreamingError(::Media::Streaming::Error &&error);
|
||||
void streamingReady(::Media::Streaming::Information &&info);
|
||||
void startStreamedPlayer();
|
||||
|
||||
const Fn<bool()> _gifPaused;
|
||||
const bool _isVideoFile;
|
||||
const FullMsgId _fullId;
|
||||
|
||||
std::shared_ptr<::Data::PhotoMedia> _photoMedia;
|
||||
std::shared_ptr<::Data::DocumentMedia> _documentMedia;
|
||||
|
||||
std::unique_ptr<::Media::Streaming::Instance> _streamed;
|
||||
|
||||
|
||||
rpl::lifetime _lifetimeDownload;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
390
Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp
Normal file
390
Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp
Normal file
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "core/mime_type.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxAlbumCount = 10;
|
||||
|
||||
} // namespace
|
||||
|
||||
PreparedFile::PreparedFile(const QString &path) : path(path) {
|
||||
}
|
||||
|
||||
PreparedFile::PreparedFile(PreparedFile &&other) = default;
|
||||
|
||||
PreparedFile &PreparedFile::operator=(PreparedFile &&other) = default;
|
||||
|
||||
PreparedFile::~PreparedFile() = default;
|
||||
|
||||
bool PreparedFile::canBeInAlbumType(AlbumType album) const {
|
||||
return CanBeInAlbumType(type, album);
|
||||
}
|
||||
|
||||
bool PreparedFile::isSticker() const {
|
||||
Expects(information != nullptr);
|
||||
|
||||
return (type == PreparedFile::Type::Photo)
|
||||
&& Core::IsMimeSticker(information->filemime);
|
||||
}
|
||||
|
||||
bool PreparedFile::isVideoFile() const {
|
||||
Expects(information != nullptr);
|
||||
|
||||
using Video = Ui::PreparedFileInformation::Video;
|
||||
return (type == PreparedFile::Type::Video)
|
||||
&& v::is<Video>(information->media)
|
||||
&& !v::get<Video>(information->media).isGifv;
|
||||
}
|
||||
|
||||
bool PreparedFile::isGifv() const {
|
||||
Expects(information != nullptr);
|
||||
|
||||
using Video = Ui::PreparedFileInformation::Video;
|
||||
return (type == PreparedFile::Type::Video)
|
||||
&& v::is<Video>(information->media)
|
||||
&& v::get<Video>(information->media).isGifv;
|
||||
}
|
||||
|
||||
AlbumType PreparedFile::albumType(bool sendImagesAsPhotos) const {
|
||||
switch (type) {
|
||||
case Type::Photo:
|
||||
return sendImagesAsPhotos ? AlbumType::PhotoVideo : AlbumType::File;
|
||||
case Type::Video:
|
||||
return AlbumType::PhotoVideo;
|
||||
case Type::Music:
|
||||
return AlbumType::Music;
|
||||
case Type::File:
|
||||
return AlbumType::File;
|
||||
case Type::None:
|
||||
return AlbumType::None;
|
||||
}
|
||||
Unexpected("PreparedFile::type in PreparedFile::albumType().");
|
||||
}
|
||||
|
||||
bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album) {
|
||||
Expects(album != AlbumType::None);
|
||||
|
||||
using Type = PreparedFile::Type;
|
||||
switch (album) {
|
||||
case AlbumType::PhotoVideo:
|
||||
return (type == Type::Photo) || (type == Type::Video);
|
||||
case AlbumType::Music:
|
||||
return (type == Type::Music);
|
||||
case AlbumType::File:
|
||||
return (type == Type::Photo) || (type == Type::File);
|
||||
}
|
||||
Unexpected("AlbumType in CanBeInAlbumType.");
|
||||
}
|
||||
|
||||
bool InsertTextOnImageCancel(const QString &text) {
|
||||
return !text.isEmpty() && !text.startsWith(u"data:image"_q);
|
||||
}
|
||||
|
||||
PreparedList PreparedList::Reordered(
|
||||
PreparedList &&list,
|
||||
std::vector<int> order) {
|
||||
Expects(list.error == PreparedList::Error::None);
|
||||
Expects(list.files.size() == order.size());
|
||||
|
||||
auto result = PreparedList(list.error, list.errorData);
|
||||
result.files.reserve(list.files.size());
|
||||
for (auto index : order) {
|
||||
result.files.push_back(std::move(list.files[index]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PreparedList::mergeToEnd(PreparedList &&other, bool cutToAlbumSize) {
|
||||
if (error != Error::None) {
|
||||
return;
|
||||
}
|
||||
if (other.error != Error::None) {
|
||||
error = other.error;
|
||||
errorData = other.errorData;
|
||||
return;
|
||||
}
|
||||
files.reserve(std::min(
|
||||
size_t(cutToAlbumSize ? kMaxAlbumCount : INT_MAX),
|
||||
files.size() + other.files.size()));
|
||||
for (auto &file : other.files) {
|
||||
if (cutToAlbumSize && files.size() == kMaxAlbumCount) {
|
||||
break;
|
||||
}
|
||||
files.push_back(std::move(file));
|
||||
}
|
||||
}
|
||||
|
||||
bool PreparedList::canBeSentInSlowmode() const {
|
||||
return canBeSentInSlowmodeWith(PreparedList());
|
||||
}
|
||||
|
||||
bool PreparedList::canBeSentInSlowmodeWith(const PreparedList &other) const {
|
||||
if (!filesToProcess.empty() || !other.filesToProcess.empty()) {
|
||||
return false;
|
||||
} else if (files.size() + other.files.size() < 2) {
|
||||
return true;
|
||||
} else if (files.size() + other.files.size() > kMaxAlbumCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
using Type = PreparedFile::Type;
|
||||
auto &&all = ranges::views::concat(files, other.files);
|
||||
const auto has = [&](Type type) {
|
||||
return ranges::contains(all, type, &PreparedFile::type);
|
||||
};
|
||||
const auto hasNonGrouping = has(Type::None);
|
||||
const auto hasPhotos = has(Type::Photo);
|
||||
const auto hasFiles = has(Type::File);
|
||||
const auto hasVideos = has(Type::Video);
|
||||
const auto hasMusic = has(Type::Music);
|
||||
|
||||
// File-s and Video-s never can be grouped.
|
||||
// Music-s can be grouped only with themselves.
|
||||
if (hasNonGrouping) {
|
||||
return false;
|
||||
} else if (hasFiles) {
|
||||
return !hasMusic && !hasVideos;
|
||||
} else if (hasVideos) {
|
||||
return !hasMusic && !hasFiles;
|
||||
} else if (hasMusic) {
|
||||
return !hasVideos && !hasFiles && !hasPhotos;
|
||||
}
|
||||
return !hasNonGrouping && (!hasFiles || !hasVideos);
|
||||
}
|
||||
|
||||
bool PreparedList::canAddCaption(bool sendingAlbum, bool compress) const {
|
||||
if (!filesToProcess.empty()
|
||||
|| files.empty()
|
||||
|| files.size() > kMaxAlbumCount) {
|
||||
return false;
|
||||
}
|
||||
if (files.size() == 1) {
|
||||
Assert(files.front().information != nullptr);
|
||||
const auto isSticker = (!compress
|
||||
&& Core::IsMimeSticker(files.front().information->filemime))
|
||||
|| files.front().path.endsWith(u".tgs"_q, Qt::CaseInsensitive);
|
||||
return !isSticker;
|
||||
} else if (!sendingAlbum) {
|
||||
return false;
|
||||
}
|
||||
const auto hasFiles = ranges::contains(
|
||||
files,
|
||||
PreparedFile::Type::File,
|
||||
&PreparedFile::type);
|
||||
const auto hasMusic = ranges::contains(
|
||||
files,
|
||||
PreparedFile::Type::Music,
|
||||
&PreparedFile::type);
|
||||
const auto hasNotGrouped = ranges::contains(
|
||||
files,
|
||||
PreparedFile::Type::None,
|
||||
&PreparedFile::type);
|
||||
return !hasFiles && !hasMusic && !hasNotGrouped;
|
||||
}
|
||||
|
||||
bool PreparedList::canMoveCaption(bool sendingAlbum, bool compress) const {
|
||||
if (!canAddCaption(sendingAlbum, compress)) {
|
||||
return false;
|
||||
} else if (files.size() != 1) {
|
||||
return true;
|
||||
}
|
||||
const auto &file = files.front();
|
||||
return (file.type == PreparedFile::Type::Video)
|
||||
|| (file.type == PreparedFile::Type::Photo && compress);
|
||||
}
|
||||
|
||||
bool PreparedList::canChangePrice(bool sendingAlbum, bool compress) const {
|
||||
return canMoveCaption(sendingAlbum, compress);
|
||||
}
|
||||
|
||||
bool PreparedList::hasGroupOption(bool slowmode) const {
|
||||
if (slowmode || files.size() < 2) {
|
||||
return false;
|
||||
}
|
||||
using Type = PreparedFile::Type;
|
||||
auto lastType = Type::None;
|
||||
for (const auto &file : files) {
|
||||
if ((file.type == lastType)
|
||||
|| (file.type == Type::Video && lastType == Type::Photo)
|
||||
|| (file.type == Type::Photo && lastType == Type::Video)
|
||||
|| (file.type == Type::File && lastType == Type::Photo)
|
||||
|| (file.type == Type::Photo && lastType == Type::File)) {
|
||||
if (lastType != Type::None) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
lastType = file.type;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PreparedList::hasSendImagesAsPhotosOption(bool slowmode) const {
|
||||
using Type = PreparedFile::Type;
|
||||
return slowmode
|
||||
? ((files.size() == 1) && (files.front().type == Type::Photo))
|
||||
: ranges::contains(files, Type::Photo, &PreparedFile::type);
|
||||
}
|
||||
|
||||
bool PreparedList::canHaveEditorHintLabel() const {
|
||||
for (const auto &file : files) {
|
||||
if ((file.type == PreparedFile::Type::Photo)
|
||||
&& !Core::IsMimeSticker(file.information->filemime)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PreparedList::hasSticker() const {
|
||||
return ranges::any_of(files, &PreparedFile::isSticker);
|
||||
}
|
||||
|
||||
bool PreparedList::hasSpoilerMenu(bool compress) const {
|
||||
const auto allAreVideo = !ranges::any_of(files, [](const auto &f) {
|
||||
using Type = Ui::PreparedFile::Type;
|
||||
return (f.type != Type::Video);
|
||||
});
|
||||
const auto allAreMedia = !ranges::any_of(files, [](const auto &f) {
|
||||
using Type = Ui::PreparedFile::Type;
|
||||
return (f.type != Type::Photo) && (f.type != Type::Video);
|
||||
});
|
||||
return allAreVideo || (allAreMedia && compress);
|
||||
}
|
||||
|
||||
std::shared_ptr<PreparedBundle> PrepareFilesBundle(
|
||||
std::vector<PreparedGroup> groups,
|
||||
SendFilesWay way,
|
||||
TextWithTags caption,
|
||||
bool ctrlShiftEnter) {
|
||||
auto totalCount = 0;
|
||||
for (const auto &group : groups) {
|
||||
totalCount += group.list.files.size();
|
||||
}
|
||||
const auto sendComment = !caption.text.isEmpty()
|
||||
&& (groups.size() != 1 || !groups.front().sentWithCaption());
|
||||
return std::make_shared<PreparedBundle>(PreparedBundle{
|
||||
.groups = std::move(groups),
|
||||
.way = way,
|
||||
.caption = std::move(caption),
|
||||
.totalCount = totalCount + (sendComment ? 1 : 0),
|
||||
.sendComment = sendComment,
|
||||
.ctrlShiftEnter = ctrlShiftEnter,
|
||||
});
|
||||
}
|
||||
|
||||
int MaxAlbumItems() {
|
||||
return kMaxAlbumCount;
|
||||
}
|
||||
|
||||
bool ValidateThumbDimensions(int width, int height) {
|
||||
return (width > 0)
|
||||
&& (height > 0)
|
||||
&& (width <= 20 * height)
|
||||
&& (height <= 20 * width);
|
||||
}
|
||||
|
||||
std::vector<PreparedGroup> DivideByGroups(
|
||||
PreparedList &&list,
|
||||
SendFilesWay way,
|
||||
bool slowmode) {
|
||||
const auto sendImagesAsPhotos = way.sendImagesAsPhotos();
|
||||
const auto groupFiles = way.groupFiles() || slowmode;
|
||||
|
||||
auto group = Ui::PreparedList();
|
||||
|
||||
using Type = Ui::PreparedFile::Type;
|
||||
auto groupType = AlbumType::None;
|
||||
|
||||
auto result = std::vector<PreparedGroup>();
|
||||
auto pushGroup = [&] {
|
||||
const auto type = (group.files.size() > 1)
|
||||
? groupType
|
||||
: AlbumType::None;
|
||||
result.push_back(PreparedGroup{
|
||||
.list = base::take(group),
|
||||
.type = type,
|
||||
});
|
||||
};
|
||||
for (auto i = 0; i != list.files.size(); ++i) {
|
||||
auto &file = list.files[i];
|
||||
const auto fileGroupType = (file.type == Type::Music)
|
||||
? (groupFiles ? AlbumType::Music : AlbumType::None)
|
||||
: (file.type == Type::Video)
|
||||
? (groupFiles ? AlbumType::PhotoVideo : AlbumType::None)
|
||||
: (file.type == Type::Photo)
|
||||
? ((groupFiles && sendImagesAsPhotos)
|
||||
? AlbumType::PhotoVideo
|
||||
: (groupFiles && !sendImagesAsPhotos)
|
||||
? AlbumType::File
|
||||
: AlbumType::None)
|
||||
: (file.type == Type::File)
|
||||
? (groupFiles ? AlbumType::File : AlbumType::None)
|
||||
: AlbumType::None;
|
||||
if ((!group.files.empty() && groupType != fileGroupType)
|
||||
|| ((groupType != AlbumType::None)
|
||||
&& (group.files.size() == Ui::MaxAlbumItems()))) {
|
||||
pushGroup();
|
||||
}
|
||||
group.files.push_back(std::move(file));
|
||||
groupType = fileGroupType;
|
||||
}
|
||||
if (!group.files.empty()) {
|
||||
pushGroup();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QPixmap PrepareSongCoverForThumbnail(QImage image, int size) {
|
||||
const auto scaledSize = image.size().scaled(
|
||||
size,
|
||||
size,
|
||||
Qt::KeepAspectRatioByExpanding);
|
||||
using Option = Images::Option;
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
return PixmapFromImage(Images::Prepare(
|
||||
std::move(image),
|
||||
scaledSize * ratio,
|
||||
{
|
||||
.colored = &st::songCoverOverlayFg,
|
||||
.options = Option::RoundCircle,
|
||||
.outer = { size, size },
|
||||
}));
|
||||
}
|
||||
|
||||
QPixmap BlurredPreviewFromPixmap(QPixmap pixmap, RectParts corners) {
|
||||
const auto image = pixmap.toImage();
|
||||
const auto skip = st::roundRadiusLarge * image.devicePixelRatio();
|
||||
auto small = image.copy(
|
||||
skip,
|
||||
skip,
|
||||
image.width() - 2 * skip,
|
||||
image.height() - 2 * skip
|
||||
).scaled(
|
||||
40,
|
||||
40,
|
||||
Qt::KeepAspectRatioByExpanding,
|
||||
Qt::SmoothTransformation);
|
||||
|
||||
using namespace Images;
|
||||
return PixmapFromImage(Prepare(
|
||||
Blur(std::move(small), true),
|
||||
image.size(),
|
||||
{ .options = RoundOptions(ImageRoundRadius::Large, corners) }));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
180
Telegram/SourceFiles/ui/chat/attach/attach_prepare.h
Normal file
180
Telegram/SourceFiles/ui/chat/attach/attach_prepare.h
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "editor/photo_editor_common.h"
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
#include "ui/rect_part.h"
|
||||
|
||||
#include <QtCore/QSemaphore>
|
||||
#include <deque>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RpWidget;
|
||||
class SendFilesWay;
|
||||
|
||||
struct PreparedFileInformation {
|
||||
struct Image {
|
||||
QImage data;
|
||||
QByteArray bytes;
|
||||
QByteArray format;
|
||||
bool animated = false;
|
||||
Editor::PhotoModifications modifications;
|
||||
};
|
||||
struct Song {
|
||||
crl::time duration = -1;
|
||||
QString title;
|
||||
QString performer;
|
||||
QImage cover;
|
||||
};
|
||||
struct Video {
|
||||
bool isGifv = false;
|
||||
bool isWebmSticker = false;
|
||||
bool supportsStreaming = false;
|
||||
crl::time duration = -1;
|
||||
QImage thumbnail;
|
||||
};
|
||||
|
||||
QString filemime;
|
||||
std::variant<v::null_t, Image, Song, Video> media;
|
||||
};
|
||||
|
||||
enum class AlbumType {
|
||||
None,
|
||||
PhotoVideo,
|
||||
Music,
|
||||
File,
|
||||
};
|
||||
|
||||
struct PreparedFile {
|
||||
// File-s can be grouped if 'groupFiles'.
|
||||
// File-s + Photo-s can be grouped if 'groupFiles && !sendImagesAsPhotos'.
|
||||
// Photo-s can be grouped if 'groupFiles'.
|
||||
// Photo-s + Video-s can be grouped if 'groupFiles && sendImagesAsPhotos'.
|
||||
// Video-s can be grouped if 'groupFiles'.
|
||||
// Music-s can be grouped if 'groupFiles'.
|
||||
enum class Type {
|
||||
None,
|
||||
Photo,
|
||||
Video,
|
||||
Music,
|
||||
File,
|
||||
};
|
||||
|
||||
PreparedFile(const QString &path);
|
||||
PreparedFile(PreparedFile &&other);
|
||||
PreparedFile &operator=(PreparedFile &&other);
|
||||
~PreparedFile();
|
||||
|
||||
[[nodiscard]] bool canBeInAlbumType(AlbumType album) const;
|
||||
[[nodiscard]] AlbumType albumType(bool sendImagesAsPhotos) const;
|
||||
[[nodiscard]] bool isSticker() const;
|
||||
[[nodiscard]] bool isVideoFile() const;
|
||||
[[nodiscard]] bool isGifv() const;
|
||||
|
||||
QString path;
|
||||
QByteArray content;
|
||||
int64 size = 0;
|
||||
std::unique_ptr<PreparedFileInformation> information;
|
||||
std::unique_ptr<PreparedFile> videoCover;
|
||||
QImage preview;
|
||||
QSize shownDimensions;
|
||||
QSize originalDimensions;
|
||||
Type type = Type::File;
|
||||
bool spoiler = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album);
|
||||
[[nodiscard]] bool InsertTextOnImageCancel(const QString &text);
|
||||
|
||||
struct PreparedList {
|
||||
enum class Error {
|
||||
None,
|
||||
NonLocalUrl,
|
||||
Directory,
|
||||
EmptyFile,
|
||||
TooLargeFile,
|
||||
};
|
||||
|
||||
PreparedList() = default;
|
||||
PreparedList(Error error, QString errorData)
|
||||
: error(error)
|
||||
, errorData(errorData) {
|
||||
}
|
||||
PreparedList(PreparedList &&other) = default;
|
||||
PreparedList &operator=(PreparedList &&other) = default;
|
||||
|
||||
[[nodiscard]] static PreparedList Reordered(
|
||||
PreparedList &&list,
|
||||
std::vector<int> order);
|
||||
void mergeToEnd(PreparedList &&other, bool cutToAlbumSize = false);
|
||||
|
||||
[[nodiscard]] bool canAddCaption(bool sendingAlbum, bool compress) const;
|
||||
[[nodiscard]] bool canMoveCaption(
|
||||
bool sendingAlbum,
|
||||
bool compress) const;
|
||||
[[nodiscard]] bool canChangePrice(
|
||||
bool sendingAlbum,
|
||||
bool compress) const;
|
||||
[[nodiscard]] bool canBeSentInSlowmode() const;
|
||||
[[nodiscard]] bool canBeSentInSlowmodeWith(
|
||||
const PreparedList &other) const;
|
||||
|
||||
[[nodiscard]] bool hasGroupOption(bool slowmode) const;
|
||||
[[nodiscard]] bool hasSendImagesAsPhotosOption(bool slowmode) const;
|
||||
[[nodiscard]] bool canHaveEditorHintLabel() const;
|
||||
[[nodiscard]] bool hasSticker() const;
|
||||
[[nodiscard]] bool hasSpoilerMenu(bool compress) const;
|
||||
|
||||
Error error = Error::None;
|
||||
QString errorData;
|
||||
std::vector<PreparedFile> files;
|
||||
std::deque<PreparedFile> filesToProcess;
|
||||
std::optional<bool> overrideSendImagesAsPhotos;
|
||||
};
|
||||
|
||||
struct PreparedGroup {
|
||||
PreparedList list;
|
||||
AlbumType type = AlbumType::None;
|
||||
|
||||
[[nodiscard]] bool sentWithCaption() const {
|
||||
return (list.files.size() == 1)
|
||||
|| (type == AlbumType::PhotoVideo);
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<PreparedGroup> DivideByGroups(
|
||||
PreparedList &&list,
|
||||
SendFilesWay way,
|
||||
bool slowmode);
|
||||
|
||||
struct PreparedBundle {
|
||||
std::vector<PreparedGroup> groups;
|
||||
SendFilesWay way;
|
||||
TextWithTags caption;
|
||||
int totalCount = 0;
|
||||
bool sendComment = false;
|
||||
bool ctrlShiftEnter = false;
|
||||
};
|
||||
[[nodiscard]] std::shared_ptr<PreparedBundle> PrepareFilesBundle(
|
||||
std::vector<PreparedGroup> groups,
|
||||
SendFilesWay way,
|
||||
TextWithTags caption,
|
||||
bool ctrlShiftEnter);
|
||||
|
||||
[[nodiscard]] int MaxAlbumItems();
|
||||
[[nodiscard]] bool ValidateThumbDimensions(int width, int height);
|
||||
|
||||
[[nodiscard]] QPixmap PrepareSongCoverForThumbnail(QImage image, int size);
|
||||
|
||||
[[nodiscard]] QPixmap BlurredPreviewFromPixmap(
|
||||
QPixmap pixmap,
|
||||
RectParts corners);
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void SendFilesWay::setSendImagesAsPhotos(bool value) {
|
||||
if (value) {
|
||||
_flags |= Flag::SendImagesAsPhotos;
|
||||
} else {
|
||||
if (hasCompressedStickers()) {
|
||||
setGroupFiles(false);
|
||||
}
|
||||
_flags &= ~Flag::SendImagesAsPhotos;
|
||||
}
|
||||
}
|
||||
|
||||
void SendFilesWay::setGroupFiles(bool value) {
|
||||
if (value) {
|
||||
_flags |= Flag::GroupFiles;
|
||||
if (hasCompressedStickers()) {
|
||||
setSendImagesAsPhotos(true);
|
||||
}
|
||||
} else {
|
||||
_flags &= ~Flag::GroupFiles;
|
||||
}
|
||||
}
|
||||
|
||||
void SendFilesWay::setHasCompressedStickers(bool value) {
|
||||
if (value) {
|
||||
_flags |= Flag::HasCompressedStickers;
|
||||
} else {
|
||||
_flags &= ~Flag::HasCompressedStickers;
|
||||
}
|
||||
}
|
||||
|
||||
//enum class SendFilesWay { // Old way. Serialize should be compatible.
|
||||
// Album,
|
||||
// Photos,
|
||||
// Files,
|
||||
//};
|
||||
|
||||
int32 SendFilesWay::serialize() const {
|
||||
auto result = (sendImagesAsPhotos() && groupFiles())
|
||||
? int32(0)
|
||||
: sendImagesAsPhotos()
|
||||
? int32(1)
|
||||
: groupFiles()
|
||||
? int32(3)
|
||||
: int32(2);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<SendFilesWay> SendFilesWay::FromSerialized(int32 value) {
|
||||
if (value < 0 || value > 3) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto result = SendFilesWay();
|
||||
result.setGroupFiles((value == 0) || (value == 3));
|
||||
result.setSendImagesAsPhotos((value == 0) || (value == 1));
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
80
Telegram/SourceFiles/ui/chat/attach/attach_send_files_way.h
Normal file
80
Telegram/SourceFiles/ui/chat/attach/attach_send_files_way.h
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/flags.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
enum class AttachActionType {
|
||||
ToggleSpoiler,
|
||||
EditCover,
|
||||
ClearCover,
|
||||
};
|
||||
|
||||
enum class AttachButtonType {
|
||||
Edit,
|
||||
Delete,
|
||||
Modify,
|
||||
None,
|
||||
};
|
||||
|
||||
class SendFilesWay final {
|
||||
public:
|
||||
[[nodiscard]] bool groupFiles() const {
|
||||
return (_flags & Flag::GroupFiles) != 0;
|
||||
}
|
||||
[[nodiscard]] bool sendImagesAsPhotos() const {
|
||||
return (_flags & Flag::SendImagesAsPhotos) != 0;
|
||||
}
|
||||
void setGroupFiles(bool value);
|
||||
void setSendImagesAsPhotos(bool value);
|
||||
void setHasCompressedStickers(bool value);
|
||||
|
||||
[[nodiscard]] inline bool operator<(const SendFilesWay &other) const {
|
||||
return _flags < other._flags;
|
||||
}
|
||||
[[nodiscard]] inline bool operator>(const SendFilesWay &other) const {
|
||||
return other < *this;
|
||||
}
|
||||
[[nodiscard]] inline bool operator<=(const SendFilesWay &other) const {
|
||||
return !(other < *this);
|
||||
}
|
||||
[[nodiscard]] inline bool operator>=(const SendFilesWay &other) const {
|
||||
return !(*this < other);
|
||||
}
|
||||
[[nodiscard]] inline bool operator==(const SendFilesWay &other) const {
|
||||
return _flags == other._flags;
|
||||
}
|
||||
[[nodiscard]] inline bool operator!=(const SendFilesWay &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
[[nodiscard]] int32 serialize() const;
|
||||
[[nodiscard]] static std::optional<SendFilesWay> FromSerialized(
|
||||
int32 value);
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool hasCompressedStickers() const {
|
||||
return (_flags & Flag::HasCompressedStickers) != 0;
|
||||
}
|
||||
|
||||
enum class Flag : uchar {
|
||||
GroupFiles = (1 << 0),
|
||||
SendImagesAsPhotos = (1 << 1),
|
||||
HasCompressedStickers = (1 << 2),
|
||||
|
||||
Default = GroupFiles | SendImagesAsPhotos,
|
||||
};
|
||||
friend inline constexpr bool is_flag_type(Flag) { return true; };
|
||||
|
||||
base::flags<Flag> _flags = Flag::Default;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_single_file_preview.h"
|
||||
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/text/format_song_name.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
#include <QtCore/QFileInfo>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
SingleFilePreview::SingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
const PreparedFile &file,
|
||||
AttachControls::Type type)
|
||||
: AbstractSingleFilePreview(parent, st, type) {
|
||||
preparePreview(file);
|
||||
}
|
||||
|
||||
void SingleFilePreview::preparePreview(const PreparedFile &file) {
|
||||
AbstractSingleFilePreview::Data data;
|
||||
|
||||
auto preview = QImage();
|
||||
if (const auto image = std::get_if<PreparedFileInformation::Image>(
|
||||
&file.information->media)) {
|
||||
preview = image->data;
|
||||
} else if (const auto video = std::get_if<PreparedFileInformation::Video>(
|
||||
&file.information->media)) {
|
||||
preview = video->thumbnail;
|
||||
}
|
||||
prepareThumbFor(data, preview);
|
||||
const auto filepath = file.path;
|
||||
if (filepath.isEmpty()) {
|
||||
auto filename = "image.png";
|
||||
data.name = filename;
|
||||
data.statusText = FormatImageSizeText(file.originalDimensions);
|
||||
data.fileIsImage = true;
|
||||
} else {
|
||||
auto fileinfo = QFileInfo(filepath);
|
||||
auto filename = fileinfo.fileName();
|
||||
data.fileIsImage = Core::FileIsImage(
|
||||
filename,
|
||||
Core::MimeTypeForFile(fileinfo).name());
|
||||
|
||||
auto songTitle = QString();
|
||||
auto songPerformer = QString();
|
||||
if (file.information) {
|
||||
if (const auto song = std::get_if<PreparedFileInformation::Song>(
|
||||
&file.information->media)) {
|
||||
songTitle = song->title;
|
||||
songPerformer = song->performer;
|
||||
data.fileIsAudio = true;
|
||||
|
||||
if (auto cover = song->cover; !cover.isNull()) {
|
||||
data.fileThumb = Ui::PrepareSongCoverForThumbnail(
|
||||
cover,
|
||||
st::attachPreviewLayout.thumbSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.name = Text::FormatSongName(filename, songTitle, songPerformer)
|
||||
.string();
|
||||
data.statusText = FormatSizeText(fileinfo.size());
|
||||
}
|
||||
|
||||
setData(data);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/chat/attach/attach_abstract_single_file_preview.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct PreparedFile;
|
||||
|
||||
class SingleFilePreview final : public AbstractSingleFilePreview {
|
||||
public:
|
||||
SingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
const PreparedFile &file,
|
||||
AttachControls::Type type = AttachControls::Type::Full);
|
||||
|
||||
private:
|
||||
void preparePreview(const PreparedFile &file);
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/chat/attach/attach_single_media_preview.h"
|
||||
|
||||
#include "editor/photo_editor_common.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "lottie/lottie_single_player.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
SingleMediaPreview *SingleMediaPreview::Create(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
const PreparedFile &file,
|
||||
Fn<bool(AttachActionType)> actionAllowed,
|
||||
AttachControls::Type type) {
|
||||
auto preview = QImage();
|
||||
auto animated = false;
|
||||
auto animationPreview = false;
|
||||
auto hasModifications = false;
|
||||
if (const auto image = std::get_if<PreparedFileInformation::Image>(
|
||||
&file.information->media)) {
|
||||
preview = Editor::ImageModified(image->data, image->modifications);
|
||||
animated = animationPreview = image->animated;
|
||||
hasModifications = !image->modifications.empty();
|
||||
} else if (const auto video = std::get_if<PreparedFileInformation::Video>(
|
||||
&file.information->media)) {
|
||||
preview = file.videoCover
|
||||
? file.videoCover->preview
|
||||
: video->thumbnail;
|
||||
animated = true;
|
||||
animationPreview = video->isGifv;
|
||||
}
|
||||
if (preview.isNull()) {
|
||||
return nullptr;
|
||||
} else if (!animated
|
||||
&& !ValidateThumbDimensions(preview.width(), preview.height())
|
||||
&& !hasModifications) {
|
||||
return nullptr;
|
||||
}
|
||||
return CreateChild<SingleMediaPreview>(
|
||||
parent,
|
||||
st,
|
||||
std::move(gifPaused),
|
||||
preview,
|
||||
animated,
|
||||
Core::IsMimeSticker(file.information->filemime),
|
||||
file.spoiler,
|
||||
animationPreview ? file.path : QString(),
|
||||
type,
|
||||
std::move(actionAllowed));
|
||||
}
|
||||
|
||||
SingleMediaPreview::SingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
QImage preview,
|
||||
bool animated,
|
||||
bool sticker,
|
||||
bool spoiler,
|
||||
const QString &animatedPreviewPath,
|
||||
AttachControls::Type type,
|
||||
Fn<bool(AttachActionType)> actionAllowed)
|
||||
: AbstractSingleMediaPreview(parent, st, type, std::move(actionAllowed))
|
||||
, _gifPaused(std::move(gifPaused))
|
||||
, _sticker(sticker) {
|
||||
Expects(!preview.isNull());
|
||||
setAnimated(animated);
|
||||
|
||||
preparePreview(preview);
|
||||
prepareAnimatedPreview(animatedPreviewPath, animated);
|
||||
setSpoiler(spoiler);
|
||||
}
|
||||
|
||||
bool SingleMediaPreview::supportsSpoilers() const {
|
||||
return !_sticker || sendWay().sendImagesAsPhotos();
|
||||
}
|
||||
|
||||
bool SingleMediaPreview::drawBackground() const {
|
||||
return !_sticker;
|
||||
}
|
||||
|
||||
bool SingleMediaPreview::tryPaintAnimation(QPainter &p) {
|
||||
if (_gifPreview && _gifPreview->started()) {
|
||||
const auto paused = _gifPaused();
|
||||
const auto frame = _gifPreview->current({
|
||||
.frame = QSize(previewWidth(), previewHeight()),
|
||||
}, paused ? 0 : crl::now());
|
||||
p.drawImage(previewLeft(), previewTop(), frame);
|
||||
return true;
|
||||
} else if (_lottiePreview && _lottiePreview->ready()) {
|
||||
const auto frame = _lottiePreview->frame();
|
||||
const auto size = frame.size() / style::DevicePixelRatio();
|
||||
p.drawImage(
|
||||
QRect(
|
||||
previewLeft() + (previewWidth() - size.width()) / 2,
|
||||
(previewHeight() - size.height()) / 2,
|
||||
size.width(),
|
||||
size.height()),
|
||||
frame);
|
||||
_lottiePreview->markFrameShown();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SingleMediaPreview::isAnimatedPreviewReady() const {
|
||||
return _gifPreview || _lottiePreview;
|
||||
}
|
||||
|
||||
void SingleMediaPreview::prepareAnimatedPreview(
|
||||
const QString &animatedPreviewPath,
|
||||
bool animated) {
|
||||
if (_sticker && animated) {
|
||||
const auto box = QSize(previewWidth(), previewHeight())
|
||||
* style::DevicePixelRatio();
|
||||
_lottiePreview = std::make_unique<Lottie::SinglePlayer>(
|
||||
Lottie::ReadContent(QByteArray(), animatedPreviewPath),
|
||||
Lottie::FrameRequest{ box });
|
||||
_lottiePreview->updates(
|
||||
) | rpl::on_next([=] {
|
||||
update();
|
||||
}, lifetime());
|
||||
} else if (!animatedPreviewPath.isEmpty()) {
|
||||
auto callback = [=](Media::Clip::Notification notification) {
|
||||
clipCallback(notification);
|
||||
};
|
||||
_gifPreview = Media::Clip::MakeReader(
|
||||
animatedPreviewPath,
|
||||
std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
void SingleMediaPreview::clipCallback(
|
||||
Media::Clip::Notification notification) {
|
||||
using namespace Media::Clip;
|
||||
switch (notification) {
|
||||
case Notification::Reinit: {
|
||||
if (_gifPreview && _gifPreview->state() == State::Error) {
|
||||
_gifPreview.setBad();
|
||||
}
|
||||
|
||||
if (_gifPreview && _gifPreview->ready() && !_gifPreview->started()) {
|
||||
_gifPreview->start({
|
||||
.frame = QSize(previewWidth(), previewHeight()),
|
||||
});
|
||||
}
|
||||
|
||||
update();
|
||||
} break;
|
||||
|
||||
case Notification::Repaint: {
|
||||
if (_gifPreview && !_gifPreview->currentDisplayed()) {
|
||||
update();
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/chat/attach/attach_abstract_single_media_preview.h"
|
||||
#include "media/clip/media_clip_reader.h"
|
||||
|
||||
namespace Lottie {
|
||||
class SinglePlayer;
|
||||
} // namespace Lottie
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct PreparedFile;
|
||||
|
||||
class SingleMediaPreview final : public AbstractSingleMediaPreview {
|
||||
public:
|
||||
static SingleMediaPreview *Create(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
const PreparedFile &file,
|
||||
Fn<bool(AttachActionType)> actionAllowed,
|
||||
AttachControls::Type type = AttachControls::Type::Full);
|
||||
|
||||
SingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
QImage preview,
|
||||
bool animated,
|
||||
bool sticker,
|
||||
bool spoiler,
|
||||
const QString &animatedPreviewPath,
|
||||
AttachControls::Type type,
|
||||
Fn<bool(AttachActionType)> actionAllowed);
|
||||
|
||||
protected:
|
||||
bool supportsSpoilers() const override;
|
||||
bool drawBackground() const override;
|
||||
bool tryPaintAnimation(QPainter &p) override;
|
||||
bool isAnimatedPreviewReady() const override;
|
||||
|
||||
private:
|
||||
void prepareAnimatedPreview(
|
||||
const QString &animatedPreviewPath,
|
||||
bool animated);
|
||||
void clipCallback(Media::Clip::Notification notification);
|
||||
|
||||
const Fn<bool()> _gifPaused;
|
||||
const bool _sticker = false;
|
||||
Media::Clip::ReaderPointer _gifPreview;
|
||||
std::unique_ptr<Lottie::SinglePlayer> _lottiePreview;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
Reference in New Issue
Block a user