mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 16:13:46 +08:00
862 lines
23 KiB
C++
862 lines
23 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "boxes/sticker_creator_box.h"
|
|
|
|
#include "api/api_stickers_creator.h"
|
|
#include "chat_helpers/compose/compose_show.h"
|
|
#include "chat_helpers/emoji_picker_overlay.h"
|
|
#include "core/file_utilities.h"
|
|
#include "data/data_document.h"
|
|
#include "data/data_document_media.h"
|
|
#include "editor/editor_layer_widget.h"
|
|
#include "editor/photo_editor.h"
|
|
#include "editor/photo_editor_common.h"
|
|
#include "editor/scene/scene.h"
|
|
#include "editor/scene/scene_item_image.h"
|
|
#include "editor/video/video_editor.h"
|
|
#include "editor/video/video_editor_common.h"
|
|
#include "editor/video/video_editor_layer.h"
|
|
#include "info/channel_statistics/boosts/giveaway/boost_badge.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "main/main_session.h"
|
|
#include "media/media_video_encode.h"
|
|
#include "media/media_video_frames.h"
|
|
#include "ui/emoji_config.h"
|
|
#include "ui/image/image.h"
|
|
#include "ui/image/image_prepare.h"
|
|
#include "ui/layers/generic_box.h"
|
|
#include "ui/layers/layer_widget.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/rect.h"
|
|
#include "ui/rp_widget.h"
|
|
#include "ui/vertical_list.h"
|
|
#include "ui/widgets/buttons.h"
|
|
#include "ui/wrap/vertical_layout.h"
|
|
#include "window/window_controller.h"
|
|
#include "window/window_session_controller.h"
|
|
#include "styles/style_chat_helpers.h"
|
|
#include "styles/style_layers.h"
|
|
|
|
#include <QtCore/QBuffer>
|
|
#include <QtCore/QDir>
|
|
#include <QtCore/QTemporaryFile>
|
|
#include <QtGui/QImageReader>
|
|
|
|
namespace {
|
|
|
|
constexpr auto kStickerSide = 512;
|
|
constexpr auto kPreviewSide = 256;
|
|
constexpr auto kWebpQuality = 95;
|
|
constexpr auto kMaxEmojis = 7;
|
|
constexpr auto kEmojiVideoMaxDuration = crl::time(3000);
|
|
constexpr auto kEmojiVideoMinDuration = crl::time(200);
|
|
constexpr auto kEmojiVideoFps = 30.;
|
|
// Server limit for a webm custom emoji file.
|
|
constexpr auto kMaxEmojiWebmBytes = 64 * 1024;
|
|
constexpr auto kMaxOriginalRatio = 3.;
|
|
constexpr auto kSquareRatioEpsilon = 0.01;
|
|
|
|
[[nodiscard]] int SideForType(Data::StickersType type) {
|
|
return (type == Data::StickersType::Emoji)
|
|
? Api::kEmojiStickerSideMax
|
|
: kStickerSide;
|
|
}
|
|
|
|
[[nodiscard]] QImage LoadImageFromFile(const QString &path) {
|
|
auto reader = QImageReader(path);
|
|
reader.setAutoTransform(true);
|
|
auto image = reader.read();
|
|
if (image.format() != QImage::Format_ARGB32_Premultiplied
|
|
&& image.format() != QImage::Format_ARGB32) {
|
|
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
|
}
|
|
return image;
|
|
}
|
|
|
|
class PreviewWidget final : public Ui::RpWidget {
|
|
public:
|
|
PreviewWidget(QWidget *parent, QImage image)
|
|
: RpWidget(parent)
|
|
, _image(std::move(image)) {
|
|
resize(kPreviewSide, kPreviewSide);
|
|
}
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent *e) override {
|
|
auto p = QPainter(this);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
const auto fitted = _image.size().scaled(
|
|
size(),
|
|
Qt::KeepAspectRatio);
|
|
p.drawImage(style::centerrect(rect(), Rect(fitted)), _image);
|
|
}
|
|
|
|
private:
|
|
const QImage _image;
|
|
|
|
};
|
|
|
|
struct EditorState {
|
|
std::shared_ptr<Image> canvas;
|
|
Editor::PhotoModifications modifications;
|
|
int side = 0;
|
|
float64 originalRatio = 0.;
|
|
};
|
|
|
|
[[nodiscard]] std::shared_ptr<EditorState> PrepareEditorState(
|
|
QImage image,
|
|
int side) {
|
|
if (image.isNull()
|
|
|| image.width() <= 0
|
|
|| image.height() <= 0
|
|
|| (image.width() > 10 * image.height())
|
|
|| (image.height() > 10 * image.width())) {
|
|
return nullptr;
|
|
}
|
|
const auto ratio = std::clamp(
|
|
image.width() / float64(image.height()),
|
|
1. / kMaxOriginalRatio,
|
|
kMaxOriginalRatio);
|
|
|
|
auto canvas = QImage(
|
|
side,
|
|
side,
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
canvas.fill(Qt::transparent);
|
|
|
|
auto scene = std::make_shared<Editor::Scene>(
|
|
QRectF(0, 0, side, side));
|
|
|
|
const auto userPixmap = QPixmap::fromImage(std::move(image));
|
|
const auto userSize = userPixmap.size();
|
|
const auto fitted = userSize.scaled(
|
|
QSize(side, side),
|
|
Qt::KeepAspectRatio);
|
|
auto itemData = Editor::ItemBase::Data{
|
|
.initialZoom = 1.0,
|
|
.zPtr = scene->lastZ(),
|
|
.size = fitted.width(),
|
|
.x = side / 2,
|
|
.y = side / 2,
|
|
.imageSize = userSize,
|
|
.contentMargins = false,
|
|
};
|
|
auto imageItem = std::make_shared<Editor::ItemImage>(
|
|
QPixmap(userPixmap),
|
|
std::move(itemData));
|
|
imageItem->setUndoable(false);
|
|
scene->addItem(std::move(imageItem));
|
|
|
|
return std::make_shared<EditorState>(EditorState{
|
|
.canvas = std::make_shared<Image>(std::move(canvas)),
|
|
.modifications = Editor::PhotoModifications{
|
|
.crop = QRect(0, 0, side, side),
|
|
.paint = std::move(scene),
|
|
},
|
|
.side = side,
|
|
.originalRatio = (std::abs(ratio - 1.) < kSquareRatioEpsilon)
|
|
? 0.
|
|
: ratio,
|
|
});
|
|
}
|
|
|
|
void ShowPhotoEditor(
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
std::shared_ptr<EditorState> state,
|
|
Fn<void(QImage&&)> onDone) {
|
|
const auto sessionController = show->resolveWindow();
|
|
if (!sessionController) {
|
|
show->showToast(tr::lng_stickers_create_open_failed(tr::now));
|
|
return;
|
|
}
|
|
const auto windowController = &sessionController->window();
|
|
const auto parentWidget = sessionController->widget();
|
|
const auto side = state->side;
|
|
|
|
auto editor = base::make_unique_q<Editor::PhotoEditor>(
|
|
parentWidget,
|
|
windowController,
|
|
state->canvas,
|
|
state->modifications,
|
|
Editor::EditorData{
|
|
.exactSize = Size(side),
|
|
.cropType = Editor::EditorData::CropType::RoundedRect,
|
|
.cropMode = Editor::EditorData::CropMode::Mask,
|
|
.originalRatio = state->originalRatio,
|
|
.keepAspectRatio = true,
|
|
.fixedCrop = true,
|
|
});
|
|
const auto raw = editor.get();
|
|
|
|
auto applyModifications = [=, done = std::move(onDone)](
|
|
const Editor::PhotoModifications &mods) mutable {
|
|
state->modifications = mods;
|
|
auto result = Editor::ImageModified(state->canvas->original(), mods);
|
|
const auto target = result.size().scaled(
|
|
Size(side),
|
|
Qt::KeepAspectRatio);
|
|
if (!target.isEmpty() && (result.size() != target)) {
|
|
result = result.scaled(
|
|
target,
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation);
|
|
}
|
|
Editor::ApplyShapeMask(result, mods);
|
|
done(std::move(result));
|
|
};
|
|
|
|
auto layer = std::make_unique<Editor::LayerWidget>(
|
|
parentWidget,
|
|
std::move(editor));
|
|
Editor::InitEditorLayer(layer.get(), raw, std::move(applyModifications));
|
|
windowController->showLayer(
|
|
std::move(layer),
|
|
Ui::LayerOption::KeepOther);
|
|
}
|
|
|
|
[[nodiscard]] QImage Sharpened(QImage image) {
|
|
constexpr auto kRadius = 1;
|
|
constexpr auto kAmount = 0.7;
|
|
if (image.isNull()) {
|
|
return image;
|
|
}
|
|
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
|
image = std::move(image).convertToFormat(
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
}
|
|
auto blurred = Images::BlurLargeImage(QImage(image), kRadius);
|
|
if (blurred.size() != image.size()) {
|
|
return image;
|
|
}
|
|
const auto width = image.width();
|
|
const auto height = image.height();
|
|
for (auto y = 0; y != height; ++y) {
|
|
const auto blur = reinterpret_cast<const QRgb*>(
|
|
blurred.constScanLine(y));
|
|
const auto line = reinterpret_cast<QRgb*>(image.scanLine(y));
|
|
for (auto x = 0; x != width; ++x) {
|
|
const auto origin = line[x];
|
|
const auto soft = blur[x];
|
|
const auto alpha = qAlpha(origin);
|
|
const auto sharp = [&](int channel, int blurChannel) {
|
|
const auto value = channel
|
|
+ int(kAmount * (channel - blurChannel));
|
|
return std::clamp(value, 0, alpha);
|
|
};
|
|
line[x] = qRgba(
|
|
sharp(qRed(origin), qRed(soft)),
|
|
sharp(qGreen(origin), qGreen(soft)),
|
|
sharp(qBlue(origin), qBlue(soft)),
|
|
alpha);
|
|
}
|
|
}
|
|
return image;
|
|
}
|
|
|
|
[[nodiscard]] QSize FittedStickerSize(QSize size, int side) {
|
|
return size.scaled(Size(side), Qt::KeepAspectRatio).expandedTo(Size(1));
|
|
}
|
|
|
|
[[nodiscard]] QByteArray EncodeWebp(QImage image, QSize size) {
|
|
if (image.size() != size) {
|
|
image = image.scaled(
|
|
size,
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation);
|
|
image = Sharpened(std::move(image));
|
|
}
|
|
if (image.format() != QImage::Format_ARGB32) {
|
|
image = image.convertToFormat(QImage::Format_ARGB32);
|
|
}
|
|
auto bytes = QByteArray();
|
|
auto buffer = QBuffer(&bytes);
|
|
buffer.open(QIODevice::WriteOnly);
|
|
image.save(&buffer, "WEBP", kWebpQuality);
|
|
return bytes;
|
|
}
|
|
|
|
void LoadStickerMedia(
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
not_null<DocumentData*> document,
|
|
Fn<void(std::shared_ptr<Data::DocumentMedia>)> done) {
|
|
struct State {
|
|
std::shared_ptr<Data::DocumentMedia> media;
|
|
rpl::lifetime lifetime;
|
|
};
|
|
const auto state = std::make_shared<State>();
|
|
state->media = document->createMediaView();
|
|
state->media->checkStickerLarge();
|
|
const auto finish = [=] {
|
|
auto media = state->media;
|
|
state->lifetime.destroy();
|
|
done(std::move(media));
|
|
};
|
|
if (state->media->loaded()) {
|
|
finish();
|
|
return;
|
|
}
|
|
show->session().downloaderTaskFinished(
|
|
) | rpl::filter([=] {
|
|
return state->media->loaded();
|
|
}) | rpl::on_next([=] {
|
|
finish();
|
|
}, state->lifetime);
|
|
}
|
|
|
|
void LoadStickerImage(
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
not_null<DocumentData*> document,
|
|
Fn<void(QImage)> done) {
|
|
LoadStickerMedia(show, document, [done = std::move(done)](
|
|
std::shared_ptr<Data::DocumentMedia> media) {
|
|
const auto large = media->getStickerLarge();
|
|
done(large ? large->original() : QImage());
|
|
});
|
|
}
|
|
|
|
struct StickerVideoSource {
|
|
std::shared_ptr<QTemporaryFile> file;
|
|
QString path;
|
|
Media::Video::FileInfo info;
|
|
Editor::VideoEditorData editorData;
|
|
Editor::VideoModifications modifications;
|
|
};
|
|
|
|
[[nodiscard]] std::shared_ptr<QTemporaryFile> WriteStickerTempFile(
|
|
const QByteArray &bytes) {
|
|
auto result = std::make_shared<QTemporaryFile>(
|
|
QDir::tempPath() + u"/tdemoji_XXXXXX.webm"_q);
|
|
if (!result->open() || result->write(bytes) != bytes.size()) {
|
|
return nullptr;
|
|
}
|
|
result->close();
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] Editor::VideoEditorData EmojiVideoEditorData() {
|
|
return {
|
|
.editor = Editor::EditorData{
|
|
.keepAspectRatio = true,
|
|
},
|
|
.exactSize = Size(Api::kEmojiStickerSideMax),
|
|
.maxDuration = kEmojiVideoMaxDuration,
|
|
.minDuration = kEmojiVideoMinDuration,
|
|
.fpsLimit = kEmojiVideoFps,
|
|
.removeAudio = true,
|
|
.webmSticker = true,
|
|
};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace Api {
|
|
namespace {
|
|
|
|
struct CreateMediaArgs {
|
|
std::shared_ptr<ChatHelpers::Show> show;
|
|
StickerSetIdentifier set;
|
|
QImage image;
|
|
std::shared_ptr<StickerVideoSource> video;
|
|
Data::StickersType type = Data::StickersType::Stickers;
|
|
std::vector<EmojiPtr> emoji;
|
|
Fn<void(std::vector<EmojiPtr>)> back;
|
|
Fn<void(MTPmessages_StickerSet)> done;
|
|
};
|
|
|
|
void CreateMediaBox(
|
|
not_null<Ui::GenericBox*> box,
|
|
CreateMediaArgs args) {
|
|
const auto show = args.show;
|
|
const auto type = args.type;
|
|
const auto back = args.back;
|
|
auto image = std::move(args.image);
|
|
const auto isEmoji = (type == Data::StickersType::Emoji);
|
|
const auto side = SideForType(type);
|
|
struct State {
|
|
rpl::variable<bool> uploading = false;
|
|
std::unique_ptr<StickerUpload> upload;
|
|
std::shared_ptr<std::atomic<bool>> cancelEncode;
|
|
QPointer<Ui::RoundButton> addButton;
|
|
};
|
|
const auto state = box->lifetime().make_state<State>();
|
|
const auto session = &show->session();
|
|
|
|
box->setTitle(isEmoji
|
|
? tr::lng_emoji_create_image_title()
|
|
: tr::lng_stickers_create_image_title());
|
|
|
|
const auto inner = box->verticalLayout();
|
|
|
|
auto pickerDescriptor = ChatHelpers::EmojiPickerOverlayDescriptor{
|
|
.aboutText = (isEmoji
|
|
? tr::lng_emoji_create_emoji_about(tr::now)
|
|
: tr::lng_stickers_create_emoji_about(tr::now)),
|
|
.maxSelected = kMaxEmojis,
|
|
.allowExpand = true,
|
|
.initialSelected = std::move(args.emoji),
|
|
};
|
|
const auto metrics = ChatHelpers::EmojiPickerOverlay::EstimateMetrics(
|
|
pickerDescriptor.aboutText);
|
|
const auto pickerCollapsed = metrics.collapsedHeight;
|
|
const auto pickerTotalExpanded = metrics.totalExpandedHeight;
|
|
const auto shadowExt = metrics.shadowExtent;
|
|
|
|
constexpr auto kStickerOverlap = 24;
|
|
const auto stickerTop = shadowExt.top()
|
|
+ pickerCollapsed
|
|
- kStickerOverlap;
|
|
const auto holderHeight = std::max(
|
|
stickerTop + kPreviewSide,
|
|
pickerTotalExpanded);
|
|
|
|
const auto previewHolder = inner->add(
|
|
object_ptr<Ui::RpWidget>(inner),
|
|
QMargins(0, 0, 0, 0),
|
|
style::al_top);
|
|
previewHolder->resize(st::boxWideWidth, holderHeight);
|
|
const auto preview = Ui::CreateChild<PreviewWidget>(
|
|
previewHolder,
|
|
image);
|
|
|
|
const auto picker = Ui::CreateChild<ChatHelpers::EmojiPickerOverlay>(
|
|
previewHolder,
|
|
std::move(pickerDescriptor));
|
|
|
|
auto layoutOverlay = [=] {
|
|
const auto bubbleW = std::min(
|
|
previewHolder->width()
|
|
- 2 * st::boxRowPadding.left()
|
|
- shadowExt.left() - shadowExt.right(),
|
|
int(kPreviewSide * 1.1));
|
|
const auto totalW = bubbleW + shadowExt.left() + shadowExt.right();
|
|
const auto x = (previewHolder->width() - totalW) / 2;
|
|
picker->setGeometry(x, 0, totalW, pickerTotalExpanded);
|
|
picker->raise();
|
|
};
|
|
|
|
previewHolder->widthValue(
|
|
) | rpl::on_next([=](int width) {
|
|
preview->move((width - kPreviewSide) / 2, stickerTop);
|
|
layoutOverlay();
|
|
}, preview->lifetime());
|
|
|
|
Ui::AddSkip(inner);
|
|
|
|
const auto video = args.video;
|
|
const auto startUpload = [=,
|
|
set = std::move(args.set),
|
|
done = std::move(args.done)]() mutable {
|
|
if (state->uploading.current()) {
|
|
return;
|
|
}
|
|
auto emoji = QString();
|
|
for (const auto one : picker->selected()) {
|
|
emoji.append(one->text());
|
|
}
|
|
if (emoji.isEmpty()) {
|
|
show->showToast(
|
|
tr::lng_stickers_create_emoji_required(tr::now));
|
|
return;
|
|
}
|
|
const auto lockUploading = [=] {
|
|
const auto lockedWidth = state->addButton
|
|
? state->addButton->width()
|
|
: 0;
|
|
state->uploading = true;
|
|
if (state->addButton && lockedWidth > 0) {
|
|
state->addButton->resizeToWidth(lockedWidth);
|
|
}
|
|
};
|
|
const auto upload = [=, doneCallback = done](
|
|
QByteArray bytes,
|
|
QSize dimensions,
|
|
crl::time videoDuration) {
|
|
state->upload = std::make_unique<StickerUpload>(
|
|
session,
|
|
set,
|
|
std::move(bytes),
|
|
dimensions,
|
|
emoji,
|
|
type,
|
|
videoDuration);
|
|
state->upload->start(
|
|
crl::guard(box, [=](MTPmessages_StickerSet result) {
|
|
state->upload = nullptr;
|
|
state->uploading = false;
|
|
show->showToast(isEmoji
|
|
? tr::lng_emoji_added(tr::now)
|
|
: tr::lng_stickers_create_added(tr::now));
|
|
if (doneCallback) {
|
|
doneCallback(result);
|
|
}
|
|
box->closeBox();
|
|
}),
|
|
crl::guard(box, [=](QString err) {
|
|
state->upload = nullptr;
|
|
state->uploading = false;
|
|
show->showToast(err.isEmpty()
|
|
? tr::lng_stickers_create_upload_failed(tr::now)
|
|
: err);
|
|
}));
|
|
};
|
|
if (video) {
|
|
lockUploading();
|
|
const auto cancel = std::make_shared<std::atomic<bool>>(false);
|
|
state->cancelEncode = cancel;
|
|
crl::async([
|
|
=,
|
|
weak = base::make_weak(box),
|
|
mods = video->modifications
|
|
] {
|
|
auto result = Media::Encode::RunWebmSticker(
|
|
Editor::ComposeVideoSource(
|
|
video->path,
|
|
mods,
|
|
video->editorData,
|
|
false),
|
|
kMaxEmojiWebmBytes,
|
|
[=](float64) { return !cancel->load(); });
|
|
crl::on_main(weak, [
|
|
=,
|
|
result = std::move(result)
|
|
]() mutable {
|
|
if (result.empty()
|
|
|| result.bytes.size() > kMaxEmojiWebmBytes) {
|
|
state->uploading = false;
|
|
if (!cancel->load()) {
|
|
show->showToast(tr::lng_bad_video(tr::now));
|
|
}
|
|
return;
|
|
}
|
|
upload(
|
|
std::move(result.bytes),
|
|
result.dimensions,
|
|
std::clamp(
|
|
result.duration,
|
|
crl::time(1),
|
|
kEmojiVideoMaxDuration));
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
const auto dimensions = FittedStickerSize(image.size(), side);
|
|
auto bytes = EncodeWebp(image, dimensions);
|
|
if (bytes.isEmpty()) {
|
|
show->showToast(
|
|
tr::lng_stickers_create_upload_failed(tr::now));
|
|
return;
|
|
}
|
|
lockUploading();
|
|
upload(std::move(bytes), dimensions, 0);
|
|
};
|
|
|
|
const auto addButton = box->addButton(
|
|
rpl::conditional(
|
|
state->uploading.value(),
|
|
rpl::single(QString()),
|
|
tr::lng_box_done()),
|
|
startUpload);
|
|
state->addButton = addButton;
|
|
box->addButton(tr::lng_cancel(), [=] {
|
|
if (back) {
|
|
back(picker->selected());
|
|
}
|
|
box->closeBox();
|
|
});
|
|
|
|
{
|
|
using namespace Info::Statistics;
|
|
const auto loadingAnimation = InfiniteRadialAnimationWidget(
|
|
addButton,
|
|
addButton->height() / 2,
|
|
&st::editStickerSetNameLoading);
|
|
AddChildToWidgetCenter(addButton, loadingAnimation);
|
|
loadingAnimation->showOn(state->uploading.value());
|
|
}
|
|
|
|
box->setWidth(st::boxWideWidth);
|
|
|
|
box->boxClosing(
|
|
) | rpl::on_next([=] {
|
|
if (state->cancelEncode) {
|
|
state->cancelEncode->store(true);
|
|
}
|
|
state->upload = nullptr;
|
|
}, box->lifetime());
|
|
}
|
|
|
|
void ShowEditorThenCreate(
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
StickerSetIdentifier set,
|
|
std::shared_ptr<EditorState> state,
|
|
Data::StickersType type,
|
|
std::vector<EmojiPtr> emoji,
|
|
Fn<void(MTPmessages_StickerSet)> done) {
|
|
ShowPhotoEditor(show, state, [=](QImage &&prepared) {
|
|
show->showBox(Box(CreateMediaBox, CreateMediaArgs{
|
|
.show = show,
|
|
.set = set,
|
|
.image = std::move(prepared),
|
|
.type = type,
|
|
.emoji = emoji,
|
|
.back = [=](std::vector<EmojiPtr> chosen) {
|
|
ShowEditorThenCreate(show, set, state, type, chosen, done);
|
|
},
|
|
.done = done,
|
|
}));
|
|
});
|
|
}
|
|
|
|
void ShowVideoEditorThenCreate(
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
StickerSetIdentifier set,
|
|
std::shared_ptr<StickerVideoSource> video,
|
|
std::vector<EmojiPtr> emoji,
|
|
Fn<void(MTPmessages_StickerSet)> done) {
|
|
const auto sessionController = show->resolveWindow();
|
|
if (!sessionController) {
|
|
show->showToast(tr::lng_stickers_create_open_failed(tr::now));
|
|
return;
|
|
}
|
|
const auto windowController = &sessionController->window();
|
|
const auto parentWidget = sessionController->widget();
|
|
|
|
auto applyModifications = [=](Editor::VideoModifications mods) {
|
|
video->modifications = mods;
|
|
const auto path = video->path;
|
|
const auto dimensions = video->info.dimensions;
|
|
crl::async([=, weak = base::make_weak(parentWidget)] {
|
|
auto preview = Editor::ExtractCoverImage(
|
|
path,
|
|
QByteArray(),
|
|
mods,
|
|
dimensions,
|
|
kPreviewSide);
|
|
crl::on_main(weak, [=, preview = std::move(preview)]() mutable {
|
|
show->showBox(Box(CreateMediaBox, CreateMediaArgs{
|
|
.show = show,
|
|
.set = set,
|
|
.image = std::move(preview),
|
|
.video = video,
|
|
.type = Data::StickersType::Emoji,
|
|
.emoji = emoji,
|
|
.back = [=](std::vector<EmojiPtr> chosen) {
|
|
ShowVideoEditorThenCreate(
|
|
show,
|
|
set,
|
|
video,
|
|
chosen,
|
|
done);
|
|
},
|
|
.done = done,
|
|
}));
|
|
});
|
|
});
|
|
};
|
|
|
|
Editor::ShowVideoEditorLayer(
|
|
parentWidget,
|
|
windowController,
|
|
Editor::VideoEditorDescriptor{
|
|
.path = video->path,
|
|
.dimensions = video->info.dimensions,
|
|
.duration = video->info.duration,
|
|
.data = video->editorData,
|
|
.initial = video->modifications,
|
|
},
|
|
std::move(applyModifications));
|
|
}
|
|
|
|
void RunVideoEditorAndCreate(
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
StickerSetIdentifier set,
|
|
not_null<DocumentData*> document,
|
|
std::shared_ptr<Data::DocumentMedia> media,
|
|
Fn<void(MTPmessages_StickerSet)> done) {
|
|
auto bytes = media->bytes();
|
|
auto file = std::shared_ptr<QTemporaryFile>();
|
|
auto path = QString();
|
|
if (!bytes.isEmpty()) {
|
|
file = WriteStickerTempFile(bytes);
|
|
path = file ? file->fileName() : QString();
|
|
}
|
|
if (path.isEmpty()) {
|
|
path = document->filepath(true);
|
|
}
|
|
const auto info = path.isEmpty()
|
|
? Media::Video::FileInfo()
|
|
: Media::Video::ReadFileInfo(path);
|
|
if (!info.valid()) {
|
|
show->showToast(tr::lng_bad_video(tr::now));
|
|
return;
|
|
}
|
|
auto video = std::make_shared<StickerVideoSource>(StickerVideoSource{
|
|
.file = std::move(file),
|
|
.path = path,
|
|
.info = info,
|
|
.editorData = EmojiVideoEditorData(),
|
|
});
|
|
auto initial = std::vector<EmojiPtr>();
|
|
if (const auto emoji = Ui::Emoji::Find(StickerEmojiOrDefault(document))) {
|
|
initial.push_back(emoji);
|
|
}
|
|
ShowVideoEditorThenCreate(
|
|
std::move(show),
|
|
std::move(set),
|
|
std::move(video),
|
|
std::move(initial),
|
|
std::move(done));
|
|
}
|
|
|
|
void RunImageEditorAndCreate(
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
StickerSetIdentifier set,
|
|
QImage image,
|
|
Data::StickersType type,
|
|
Fn<void(MTPmessages_StickerSet)> done) {
|
|
auto state = PrepareEditorState(std::move(image), kStickerSide);
|
|
if (!state) {
|
|
show->showToast(tr::lng_stickers_create_open_failed(tr::now));
|
|
return;
|
|
}
|
|
if (type != Data::StickersType::Stickers) {
|
|
state->originalRatio = 0.;
|
|
}
|
|
ShowEditorThenCreate(
|
|
std::move(show),
|
|
std::move(set),
|
|
std::move(state),
|
|
type,
|
|
{},
|
|
std::move(done));
|
|
}
|
|
|
|
void ChooseImageThenCreate(
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
StickerSetIdentifier set,
|
|
Data::StickersType type,
|
|
Fn<void(MTPmessages_StickerSet)> done) {
|
|
const auto parent = QPointer<QWidget>(show->toastParent());
|
|
|
|
const auto onChosen = [=, set = std::move(set), done = std::move(done)](
|
|
FileDialog::OpenResult &&result) mutable {
|
|
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
|
|
return;
|
|
}
|
|
const auto path = result.paths.isEmpty()
|
|
? QString()
|
|
: result.paths.front();
|
|
auto image = path.isEmpty()
|
|
? QImage::fromData(result.remoteContent)
|
|
: LoadImageFromFile(path);
|
|
RunImageEditorAndCreate(
|
|
show,
|
|
std::move(set),
|
|
std::move(image),
|
|
type,
|
|
std::move(done));
|
|
};
|
|
|
|
FileDialog::GetOpenPath(
|
|
parent,
|
|
tr::lng_stickers_create_choose_image(tr::now),
|
|
FileDialog::ImagesFilter(),
|
|
std::move(onChosen));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void OpenCreateStickerFlow(
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
StickerSetIdentifier set,
|
|
Fn<void(MTPmessages_StickerSet)> done) {
|
|
ChooseImageThenCreate(
|
|
std::move(show),
|
|
std::move(set),
|
|
Data::StickersType::Stickers,
|
|
std::move(done));
|
|
}
|
|
|
|
void OpenCreateEmojiFlow(
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
StickerSetIdentifier set,
|
|
Fn<void(MTPmessages_StickerSet)> done) {
|
|
ChooseImageThenCreate(
|
|
std::move(show),
|
|
std::move(set),
|
|
Data::StickersType::Emoji,
|
|
std::move(done));
|
|
}
|
|
|
|
bool AdaptStickerToEmoji(
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
StickerSetIdentifier set,
|
|
not_null<DocumentData*> document,
|
|
Fn<void(MTPmessages_StickerSet)> done) {
|
|
const auto sticker = document->sticker();
|
|
if (!sticker) {
|
|
show->showToast(tr::lng_attach_failed(tr::now));
|
|
return false;
|
|
}
|
|
if (sticker->isLottie()) {
|
|
const auto emoji = StickerEmojiOrDefault(document);
|
|
AddExistingStickerToSet(
|
|
&show->session(),
|
|
set,
|
|
document,
|
|
emoji,
|
|
[=](MTPmessages_StickerSet result) {
|
|
show->showToast(tr::lng_emoji_added(tr::now));
|
|
if (done) {
|
|
done(result);
|
|
}
|
|
},
|
|
[=](QString err) {
|
|
show->showToast(err.isEmpty()
|
|
? tr::lng_attach_failed(tr::now)
|
|
: err);
|
|
});
|
|
return true;
|
|
}
|
|
if (sticker->isWebm()) {
|
|
LoadStickerMedia(
|
|
show,
|
|
document,
|
|
[=, set = std::move(set), done = std::move(done)](
|
|
std::shared_ptr<Data::DocumentMedia> media) mutable {
|
|
RunVideoEditorAndCreate(
|
|
show,
|
|
std::move(set),
|
|
document,
|
|
std::move(media),
|
|
std::move(done));
|
|
});
|
|
return true;
|
|
}
|
|
LoadStickerImage(
|
|
show,
|
|
document,
|
|
[=, set = std::move(set), done = std::move(done)](
|
|
QImage image) mutable {
|
|
if (image.isNull()) {
|
|
show->showToast(
|
|
tr::lng_stickers_create_open_failed(tr::now));
|
|
return;
|
|
}
|
|
RunImageEditorAndCreate(
|
|
show,
|
|
std::move(set),
|
|
std::move(image),
|
|
Data::StickersType::Emoji,
|
|
std::move(done));
|
|
});
|
|
return true;
|
|
}
|
|
|
|
} // namespace Api
|