From 2589a244dddb9aed64d713562a3237c4b62e1d1c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 14 Apr 2026 08:35:47 +0300 Subject: [PATCH] Added flow for creating new static webp sticker. --- Telegram/CMakeLists.txt | 2 + .../SourceFiles/boxes/sticker_creator_box.cpp | 333 ++++++++++++++++++ .../SourceFiles/boxes/sticker_creator_box.h | 78 ++++ .../SourceFiles/boxes/sticker_set_box.cpp | 15 +- 4 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 Telegram/SourceFiles/boxes/sticker_creator_box.cpp create mode 100644 Telegram/SourceFiles/boxes/sticker_creator_box.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 21278791f4..d29a4281ee 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -376,6 +376,8 @@ PRIVATE boxes/star_gift_preview_box.h boxes/star_gift_resale_box.cpp boxes/star_gift_resale_box.h + boxes/sticker_creator_box.cpp + boxes/sticker_creator_box.h boxes/sticker_picker_box.cpp boxes/sticker_picker_box.h boxes/sticker_set_box.cpp diff --git a/Telegram/SourceFiles/boxes/sticker_creator_box.cpp b/Telegram/SourceFiles/boxes/sticker_creator_box.cpp new file mode 100644 index 0000000000..25308baf65 --- /dev/null +++ b/Telegram/SourceFiles/boxes/sticker_creator_box.cpp @@ -0,0 +1,333 @@ +/* +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 "core/file_utilities.h" +#include "editor/editor_layer_widget.h" +#include "editor/photo_editor.h" +#include "editor/photo_editor_common.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/emoji_config.h" +#include "ui/image/image.h" +#include "ui/image/image_prepare.h" +#include "ui/layers/box_content.h" +#include "ui/layers/layer_widget.h" +#include "ui/painter.h" +#include "ui/rp_widget.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" + +#include +#include + +namespace { + +constexpr auto kStickerSide = 512; +constexpr auto kPreviewSide = 256; +constexpr auto kWebpQuality = 95; + +[[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 target = QRect(0, 0, width(), height()); + p.drawImage(target, _image); + } + +private: + const QImage _image; + +}; + +void OpenPhotoEditorForSticker( + std::shared_ptr show, + QImage image, + Fn onDone) { + if (image.isNull()) { + show->showToast(tr::lng_stickers_create_open_failed(tr::now)); + return; + } + 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 minSide = std::min(image.width(), image.height()); + if (minSide < kStickerSide) { + show->showToast(tr::lng_stickers_create_too_small( + tr::now, + lt_size, + QString::number(kStickerSide))); + return; + } + if ((image.width() > 10 * image.height()) + || (image.height() > 10 * image.width())) { + show->showToast(tr::lng_stickers_create_open_failed(tr::now)); + return; + } + + const auto fileImage = std::make_shared(std::move(image)); + const auto initialCrop = [&] { + const auto i = fileImage; + const auto side = std::min(i->width(), i->height()); + return QRect( + (i->width() - side) / 2, + (i->height() - side) / 2, + side, + side); + }(); + + auto editor = base::make_unique_q( + parentWidget, + windowController, + fileImage, + Editor::PhotoModifications{ .crop = initialCrop }, + Editor::EditorData{ .exactSize = QSize(kStickerSide, kStickerSide) }); + const auto raw = editor.get(); + + auto applyModifications = [=, done = std::move(onDone)]( + const Editor::PhotoModifications &mods) mutable { + auto result = Editor::ImageModified(fileImage->original(), mods); + if (result.size() != QSize(kStickerSide, kStickerSide)) { + result = result.scaled( + kStickerSide, + kStickerSide, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + } + done(std::move(result)); + }; + + auto layer = std::make_unique( + parentWidget, + std::move(editor)); + Editor::InitEditorLayer(layer.get(), raw, std::move(applyModifications)); + windowController->showLayer( + std::move(layer), + Ui::LayerOption::KeepOther); +} + +} // namespace + +StickerCreatorBox::StickerCreatorBox( + QWidget*, + std::shared_ptr show, + StickerSetIdentifier set, + QImage image, + Fn done) +: _show(std::move(show)) +, _session(&_show->session()) +, _set(std::move(set)) +, _image(std::move(image)) +, _done(std::move(done)) { +} + +StickerCreatorBox::~StickerCreatorBox() = default; + +void StickerCreatorBox::prepare() { + setTitle(tr::lng_stickers_create_image_title()); + + const auto inner = setInnerWidget( + object_ptr(this)); + inner->resizeToWidth(st::boxWideWidth); + + const auto previewHolder = inner->add( + object_ptr(inner), + QMargins(0, st::boxRowPadding.left(), 0, st::boxRowPadding.left()), + style::al_top); + previewHolder->resize(st::boxWideWidth, kPreviewSide); + const auto preview = Ui::CreateChild( + previewHolder, + _image); + previewHolder->widthValue( + ) | rpl::on_next([=](int width) { + preview->move((width - kPreviewSide) / 2, 0); + }, preview->lifetime()); + + inner->add( + object_ptr( + inner, + tr::lng_stickers_create_choose_emoji(), + st::boxLabel), + st::boxRowPadding); + + _emojiField = inner->add( + object_ptr( + inner, + st::editStickerSetNameField, + tr::lng_stickers_create_choose_emoji(), + QString()), + st::boxRowPadding); + _emojiField->setMaxLength(32); + + _status = inner->add( + object_ptr( + inner, + QString(), + st::boxLabel), + st::boxRowPadding); + _status->hide(); + + _addButton = addButton( + tr::lng_box_done(), + [=] { startUpload(); }); + addButton(tr::lng_cancel(), [=] { closeBox(); }); + + setDimensionsToContent(st::boxWideWidth, inner); +} + +QByteArray StickerCreatorBox::encodeWebp() const { + auto image = _image; + if (image.size() != QSize(kStickerSide, kStickerSide)) { + image = image.scaled( + kStickerSide, + kStickerSide, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + } + 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 StickerCreatorBox::setState(State state) { + _state = state; + const auto uploading = (state == State::Uploading); + _emojiField->setEnabled(!uploading); + if (uploading) { + _status->show(); + _status->setText(tr::lng_stickers_create_uploading(tr::now)); + } else { + _status->setText(QString()); + _status->hide(); + } +} + +void StickerCreatorBox::startUpload() { + if (_state == State::Uploading) { + return; + } + const auto text = _emojiField->getLastText().trimmed(); + auto emojiLen = 0; + const auto emoji = Ui::Emoji::Find(text, &emojiLen); + if (!emoji || emojiLen != text.size()) { + _emojiField->showError(); + _show->showToast(tr::lng_stickers_create_emoji_required(tr::now)); + return; + } + const auto bytes = encodeWebp(); + if (bytes.isEmpty()) { + _show->showToast(tr::lng_stickers_create_upload_failed(tr::now)); + return; + } + + setState(State::Uploading); + _upload = std::make_unique( + _session, + _set, + bytes, + emoji->text()); + + const auto show = _show; + const auto doneCallback = _done; + _upload->start( + crl::guard(this, [=, this](MTPmessages_StickerSet result) { + _upload = nullptr; + show->showToast(tr::lng_stickers_create_added(tr::now)); + if (doneCallback) { + doneCallback(result); + } + closeBox(); + }), + crl::guard(this, [=, this](QString err) { + _upload = nullptr; + setState(State::ChooseEmoji); + show->showToast(err.isEmpty() + ? tr::lng_stickers_create_upload_failed(tr::now) + : err); + })); +} + +namespace Api { + +void OpenCreateStickerFlow( + std::shared_ptr show, + StickerSetIdentifier set, + Fn done) { + const auto parent = QPointer(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); + OpenPhotoEditorForSticker( + show, + std::move(image), + [=, set = std::move(set), done = std::move(done)]( + QImage &&prepared) mutable { + show->showBox(Box( + show, + std::move(set), + std::move(prepared), + std::move(done))); + }); + }; + + FileDialog::GetOpenPath( + parent, + tr::lng_stickers_create_choose_image(tr::now), + FileDialog::ImagesFilter(), + std::move(onChosen)); +} + +} // namespace Api diff --git a/Telegram/SourceFiles/boxes/sticker_creator_box.h b/Telegram/SourceFiles/boxes/sticker_creator_box.h new file mode 100644 index 0000000000..8966ec48aa --- /dev/null +++ b/Telegram/SourceFiles/boxes/sticker_creator_box.h @@ -0,0 +1,78 @@ +/* +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 "data/stickers/data_stickers.h" +#include "ui/layers/box_content.h" + +#include + +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class FlatLabel; +class InputField; +class RoundButton; +} // namespace Ui + +namespace Api { +class StickerUpload; +} // namespace Api + +class StickerCreatorBox final : public Ui::BoxContent { +public: + StickerCreatorBox( + QWidget*, + std::shared_ptr show, + StickerSetIdentifier set, + QImage image, + Fn done); + ~StickerCreatorBox(); + +protected: + void prepare() override; + +private: + enum class State { + ChooseEmoji, + Uploading, + }; + + void setState(State state); + void startUpload(); + [[nodiscard]] QByteArray encodeWebp() const; + + const std::shared_ptr _show; + const not_null _session; + const StickerSetIdentifier _set; + const QImage _image; + const Fn _done; + + State _state = State::ChooseEmoji; + Ui::InputField *_emojiField = nullptr; + Ui::FlatLabel *_status = nullptr; + Ui::RoundButton *_addButton = nullptr; + + std::unique_ptr _upload; + +}; + +namespace Api { + +void OpenCreateStickerFlow( + std::shared_ptr show, + StickerSetIdentifier set, + Fn done = nullptr); + +} // namespace Api diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 642e640967..0ea2e41d6c 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/unixtime.h" #include "boxes/premium_preview_box.h" +#include "boxes/sticker_creator_box.h" #include "boxes/sticker_picker_box.h" #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/stickers_list_widget.h" @@ -2386,7 +2387,19 @@ void StickerSetBox::Inner::startAddExistingStickerFlow() { } void StickerSetBox::Inner::startCreateNewStickerFlow() { - _show->showToast(u"Create a new sticker: not implemented yet."_q); + if (!hasAddCell()) { + return; + } + const auto identifier = StickerSetIdentifier{ + .id = _setId, + .accessHash = _setAccessHash, + .shortName = _setShortName, + }; + const auto onDone = crl::guard(this, [=, this]( + MTPmessages_StickerSet result) { + applySet(result); + }); + Api::OpenCreateStickerFlow(_show, identifier, onDone); } StickerSetBox::Inner::~Inner() = default;