tdesktop/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp
2023-07-20 07:20:09 +04:00

1022 lines
28 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 "window/themes/window_theme_editor_box.h"
#include "window/themes/window_theme.h"
#include "window/themes/window_theme_editor.h"
#include "window/themes/window_theme_preview.h"
#include "window/themes/window_themes_generate_name.h"
#include "window/window_controller.h"
#include "ui/boxes/confirm_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/image/image_prepare.h"
#include "ui/toast/toast.h"
#include "ui/text/format_values.h"
#include "ui/style/style_palette_colorizer.h"
#include "ui/widgets/fields/special_fields.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "main/main_account.h"
#include "main/main_session.h"
#include "storage/localstorage.h"
#include "core/file_utilities.h"
#include "core/application.h"
#include "lang/lang_keys.h"
#include "base/event_filter.h"
#include "base/base_file_utilities.h"
#include "base/zlib_help.h"
#include "base/unixtime.h"
#include "base/random.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_cloud_themes.h"
#include "storage/file_upload.h"
#include "mainwindow.h"
#include "apiwrap.h"
#include "styles/style_widgets.h"
#include "styles/style_window.h"
#include "styles/style_settings.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include <QtCore/QBuffer>
namespace Window {
namespace Theme {
namespace {
constexpr auto kRandomSlugSize = 16;
constexpr auto kMinSlugSize = 5;
constexpr auto kMaxSlugSize = 64;
enum class SaveErrorType {
Other,
Name,
Link,
};
class BackgroundSelector : public Ui::RpWidget {
public:
BackgroundSelector(
QWidget *parent,
const QImage &background,
const ParsedTheme &parsed);
[[nodiscard]] ParsedTheme result() const;
[[nodiscard]] QImage image() const;
int resizeGetHeight(int newWidth) override;
protected:
void paintEvent(QPaintEvent *e) override;
private:
void updateThumbnail();
void chooseBackgroundFromFile();
object_ptr<Ui::LinkButton> _chooseFromFile;
object_ptr<Ui::Checkbox> _tileBackground;
QImage _background;
ParsedTheme _parsed;
QString _imageText;
int _thumbnailSize = 0;
QPixmap _thumbnail;
};
template <size_t Size>
QByteArray qba(const char(&string)[Size]) {
return QByteArray::fromRawData(string, Size - 1);
}
QByteArray qba(QLatin1String string) {
return QByteArray::fromRawData(string.data(), string.size());
}
BackgroundSelector::BackgroundSelector(
QWidget *parent,
const QImage &background,
const ParsedTheme &parsed)
: RpWidget(parent)
, _chooseFromFile(
this,
tr::lng_settings_bg_from_file(tr::now),
st::boxLinkButton)
, _tileBackground(
this,
tr::lng_settings_bg_tile(tr::now),
parsed.tiled,
st::defaultBoxCheckbox)
, _background(background)
, _parsed(parsed) {
_imageText = tr::lng_theme_editor_saved_to_jpg(
tr::now,
lt_size,
Ui::FormatSizeText(_parsed.background.size()));
_chooseFromFile->setClickedCallback([=] { chooseBackgroundFromFile(); });
_thumbnailSize = st::boxTextFont->height
+ st::themesSmallSkip
+ _chooseFromFile->heightNoMargins()
+ st::themesSmallSkip
+ _tileBackground->heightNoMargins();
resize(width(), _thumbnailSize + st::themesSmallSkip);
updateThumbnail();
}
void BackgroundSelector::paintEvent(QPaintEvent *e) {
Painter p(this);
const auto left = _thumbnailSize + st::themesSmallSkip;
p.setPen(st::boxTextFg);
p.setFont(st::boxTextFont);
p.drawTextLeft(left, 0, width(), _imageText);
p.drawPixmapLeft(0, 0, width(), _thumbnail);
}
int BackgroundSelector::resizeGetHeight(int newWidth) {
const auto left = _thumbnailSize + st::themesSmallSkip;
_chooseFromFile->moveToLeft(left, st::boxTextFont->height + st::themesSmallSkip);
_tileBackground->moveToLeft(left, st::boxTextFont->height + st::themesSmallSkip + _chooseFromFile->height() + st::themesSmallSkip);
return height();
}
void BackgroundSelector::updateThumbnail() {
const auto size = _thumbnailSize;
auto back = QImage(
QSize(size, size) * cIntRetinaFactor(),
QImage::Format_ARGB32_Premultiplied);
back.setDevicePixelRatio(cRetinaFactor());
{
Painter p(&back);
PainterHighQualityEnabler hq(p);
auto &pix = _background;
int sx = (pix.width() > pix.height()) ? ((pix.width() - pix.height()) / 2) : 0;
int sy = (pix.height() > pix.width()) ? ((pix.height() - pix.width()) / 2) : 0;
int s = (pix.width() > pix.height()) ? pix.height() : pix.width();
p.drawImage(QRect(0, 0, size, size), pix, QRect(sx, sy, s, s));
}
_thumbnail = Ui::PixmapFromImage(
Images::Round(std::move(back), ImageRoundRadius::Small));
_thumbnail.setDevicePixelRatio(cRetinaFactor());
update();
}
void BackgroundSelector::chooseBackgroundFromFile() {
const auto callback = [=](const FileDialog::OpenResult &result) {
auto content = result.remoteContent;
if (!result.paths.isEmpty()) {
QFile f(result.paths.front());
if (f.open(QIODevice::ReadOnly)) {
content = f.readAll();
f.close();
}
}
if (!content.isEmpty()) {
auto read = Images::Read({ .content = content });
if (!read.image.isNull()
&& (read.format == "jpeg"
|| read.format == "jpg"
|| read.format == "png")) {
_background = std::move(read.image);
_parsed.background = content;
_parsed.isPng = (read.format == "png");
const auto phrase = _parsed.isPng
? tr::lng_theme_editor_read_from_png
: tr::lng_theme_editor_read_from_jpg;
_imageText = phrase(
tr::now,
lt_size,
Ui::FormatSizeText(_parsed.background.size()));
_tileBackground->setChecked(false);
updateThumbnail();
}
}
};
FileDialog::GetOpenPath(
this,
tr::lng_theme_editor_choose_image(tr::now),
"Image files (*.jpeg *.jpg *.png)",
crl::guard(this, callback));
}
ParsedTheme BackgroundSelector::result() const {
auto result = _parsed;
result.tiled = _tileBackground->checked();
return result;
}
QImage BackgroundSelector::image() const {
return _background;
}
bool PaletteChanged(
const QByteArray &editorPalette,
const QByteArray &originalPalette,
const Data::CloudTheme &cloud) {
return originalPalette.isEmpty()
|| (editorPalette != WriteCloudToText(cloud) + originalPalette);
}
void ImportFromFile(
not_null<Main::Session*> session,
not_null<QWidget*> parent) {
auto filters = QStringList(
u"Theme files (*.tdesktop-theme *.tdesktop-palette)"_q);
filters.push_back(FileDialog::AllFilesFilter());
const auto callback = crl::guard(session, [=](
const FileDialog::OpenResult &result) {
const auto path = result.paths.isEmpty()
? QString()
: result.paths.front();
if (!path.isEmpty()) {
Window::Theme::Apply(path);
}
});
FileDialog::GetOpenPath(
parent.get(),
tr::lng_theme_editor_menu_import(tr::now),
filters.join(u";;"_q),
crl::guard(parent, callback));
}
// They're duplicated in window_theme.cpp:ChatBackground::ChatBackground.
[[nodiscard]] QByteArray ReplaceAdjustableColors(QByteArray data) {
const auto &themeObject = Background()->themeObject();
const auto &paper = Background()->paper();
const auto usingDefaultTheme = themeObject.pathAbsolute.isEmpty();
const auto usingThemeBackground = usingDefaultTheme
? Data::IsDefaultWallPaper(paper)
: Data::IsThemeWallPaper(paper);
if (usingThemeBackground) {
return data;
}
const auto adjustables = base::flat_map<QByteArray, style::color>{
{ qba(qstr("msgServiceBg")), st::msgServiceBg },
{ qba(qstr("msgServiceBgSelected")), st::msgServiceBgSelected },
{ qba(qstr("historyScrollBg")), st::historyScrollBg },
{ qba(qstr("historyScrollBgOver")), st::historyScrollBgOver },
{ qba(qstr("historyScrollBarBg")), st::historyScrollBarBg },
{ qba(qstr("historyScrollBarBgOver")), st::historyScrollBarBgOver }
};
for (const auto &[name, color] : adjustables) {
data = ReplaceValueInPaletteContent(
data,
name,
ColorHexString(color->c));
if (data == "error") {
LOG(("Theme Error: could not adjust '%1: %2' in content").arg(
QString::fromLatin1(name),
QString::fromLatin1(ColorHexString(color->c))));
return QByteArray();
}
}
return data;
}
QByteArray GenerateDefaultPalette() {
auto result = QByteArray();
const auto rows = style::main_palette::data();
for (const auto &row : std::as_const(rows)) {
result.append(qba(row.name)
).append(": "
).append(qba(row.value)
).append("; // "
).append(
qba(
row.description
).replace(
'\n',
' '
).replace(
'\r',
' ')
).append('\n');
}
return result;
}
bool CopyColorsToPalette(
const QString &path,
const QByteArray &palette,
const Data::CloudTheme &cloud) {
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) {
LOG(("Theme Error: could not open '%1' for writing.").arg(path));
return false;
}
const auto prefix = WriteCloudToText(cloud);
if (f.write(prefix) != prefix.size()
|| f.write(palette) != palette.size()) {
LOG(("Theme Error: could not write palette to '%1'").arg(path));
return false;
}
return true;
}
[[nodiscard]] QByteArray PackTheme(const ParsedTheme &parsed) {
zlib::FileToWrite zip;
zip_fileinfo zfi = { { 0, 0, 0, 0, 0, 0 }, 0, 0, 0 };
const auto back = std::string(parsed.tiled ? "tiled" : "background")
+ (parsed.isPng ? ".png" : ".jpg");
zip.openNewFile(
back.c_str(),
&zfi,
nullptr,
0,
nullptr,
0,
nullptr,
Z_DEFLATED,
Z_DEFAULT_COMPRESSION);
zip.writeInFile(
parsed.background.constData(),
parsed.background.size());
zip.closeFile();
const auto scheme = "colors.tdesktop-theme";
zip.openNewFile(
scheme,
&zfi,
nullptr,
0,
nullptr,
0,
nullptr,
Z_DEFLATED,
Z_DEFAULT_COMPRESSION);
zip.writeInFile(parsed.palette.constData(), parsed.palette.size());
zip.closeFile();
zip.close();
if (zip.error() != ZIP_OK) {
LOG(("Theme Error: could not export zip-ed theme, status: %1"
).arg(zip.error()));
return QByteArray();
}
return zip.result();
}
[[nodiscard]] bool IsGoodSlug(const QString &slug) {
if (slug.size() < kMinSlugSize || slug.size() > kMaxSlugSize) {
return false;
}
return ranges::none_of(slug, [](QChar ch) {
return (ch < 'A' || ch > 'Z')
&& (ch < 'a' || ch > 'z')
&& (ch < '0' || ch > '9')
&& (ch != '_');
});
}
SendMediaReady PrepareThemeMedia(
MTP::DcId dcId,
const QString &name,
const QByteArray &content) {
PreparedPhotoThumbs thumbnails;
QVector<MTPPhotoSize> sizes;
auto thumbnail = GeneratePreview(content, QString()).scaled(
320,
320,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
auto thumbnailBytes = QByteArray();
{
QBuffer buffer(&thumbnailBytes);
thumbnail.save(&buffer, "JPG", 87);
}
const auto push = [&](
const char *type,
QImage &&image,
QByteArray bytes = QByteArray()) {
sizes.push_back(MTP_photoSize(
MTP_string(type),
MTP_int(image.width()),
MTP_int(image.height()), MTP_int(0)));
thumbnails.emplace(type[0], PreparedPhotoThumb{
.image = std::move(image),
.bytes = std::move(bytes)
});
};
push("s", std::move(thumbnail), thumbnailBytes);
const auto filename = base::FileNameFromUserString(name)
+ u".tdesktop-theme"_q;
auto attributes = QVector<MTPDocumentAttribute>(
1,
MTP_documentAttributeFilename(MTP_string(filename)));
const auto id = base::RandomValue<DocumentId>();
const auto document = MTP_document(
MTP_flags(0),
MTP_long(id),
MTP_long(0),
MTP_bytes(),
MTP_int(base::unixtime::now()),
MTP_string("application/x-tgtheme-tdesktop"),
MTP_long(content.size()),
MTP_vector<MTPPhotoSize>(sizes),
MTPVector<MTPVideoSize>(),
MTP_int(dcId),
MTP_vector<MTPDocumentAttribute>(attributes));
return SendMediaReady(
SendMediaType::ThemeFile,
QString(), // filepath
filename,
content.size(),
content,
id,
0,
QString(),
PeerId(),
MTP_photoEmpty(MTP_long(0)),
thumbnails,
document,
thumbnailBytes);
}
Fn<void()> SavePreparedTheme(
not_null<Window::Controller*> window,
const ParsedTheme &parsed,
const QImage &background,
const QByteArray &originalContent,
const ParsedTheme &originalParsed,
const Data::CloudTheme &fields,
Fn<void()> done,
Fn<void(SaveErrorType,QString)> fail) {
Expects(window->account().sessionExists());
using Storage::UploadedMedia;
struct State {
FullMsgId id;
bool generating = false;
mtpRequestId requestId = 0;
QByteArray themeContent;
QString filename;
rpl::lifetime lifetime;
};
const auto session = &window->account().session();
const auto api = &session->api();
const auto state = std::make_shared<State>();
state->id = FullMsgId(
session->userPeerId(),
session->data().nextLocalMessageId());
const auto creating = !fields.id
|| (fields.createdBy != session->userId());
const auto changed = (parsed.background != originalParsed.background)
|| (parsed.tiled != originalParsed.tiled)
|| PaletteChanged(parsed.palette, originalParsed.palette, fields);
const auto finish = [=](const MTPTheme &result) {
Background()->clearEditingTheme(ClearEditing::KeepChanges);
done();
const auto cloud = result.match([&](const MTPDtheme &data) {
const auto result = Data::CloudTheme::Parse(session, data);
session->data().cloudThemes().savedFromEditor(result);
return result;
});
if (cloud.documentId && !state->themeContent.isEmpty()) {
const auto document = session->data().document(cloud.documentId);
document->setDataAndCache(state->themeContent);
}
KeepFromEditor(
originalContent,
originalParsed,
cloud,
state->themeContent,
parsed,
background);
};
const auto createTheme = [=](const MTPDocument &data) {
const auto document = session->data().processDocument(data);
state->requestId = api->request(MTPaccount_CreateTheme(
MTP_flags(MTPaccount_CreateTheme::Flag::f_document),
MTP_string(fields.slug),
MTP_string(fields.title),
document->mtpInput(),
MTPVector<MTPInputThemeSettings>()
)).done([=](const MTPTheme &result) {
finish(result);
}).fail([=](const MTP::Error &error) {
fail(SaveErrorType::Other, error.type());
}).send();
};
const auto updateTheme = [=](const MTPDocument &data) {
using Flag = MTPaccount_UpdateTheme::Flag;
const auto document = session->data().processDocument(data);
const auto flags = Flag::f_title
| Flag::f_slug
| (data.type() == mtpc_documentEmpty
? Flag(0)
: Flag::f_document);
state->requestId = api->request(MTPaccount_UpdateTheme(
MTP_flags(flags),
MTP_string(Data::CloudThemes::Format()),
MTP_inputTheme(MTP_long(fields.id), MTP_long(fields.accessHash)),
MTP_string(fields.slug),
MTP_string(fields.title),
document->mtpInput(),
MTPVector<MTPInputThemeSettings>()
)).done([=](const MTPTheme &result) {
finish(result);
}).fail([=](const MTP::Error &error) {
fail(SaveErrorType::Other, error.type());
}).send();
};
const auto uploadTheme = [=](const UploadedMedia &data) {
state->requestId = api->request(MTPaccount_UploadTheme(
MTP_flags(MTPaccount_UploadTheme::Flag::f_thumb),
data.info.file,
*data.info.thumb,
MTP_string(state->filename),
MTP_string("application/x-tgtheme-tdesktop")
)).done([=](const MTPDocument &result) {
if (creating) {
createTheme(result);
} else {
updateTheme(result);
}
}).fail([=](const MTP::Error &error) {
fail(SaveErrorType::Other, error.type());
}).send();
};
const auto uploadFile = [=](const QByteArray &theme) {
const auto media = PrepareThemeMedia(
session->mainDcId(),
fields.title,
theme);
state->filename = media.filename;
state->themeContent = theme;
session->uploader().documentReady(
) | rpl::filter([=](const UploadedMedia &data) {
return (data.fullId == state->id) && data.info.thumb.has_value();
}) | rpl::start_with_next([=](const UploadedMedia &data) {
uploadTheme(data);
}, state->lifetime);
session->uploader().uploadMedia(state->id, media);
};
const auto save = [=] {
if (!creating && !changed) {
updateTheme(MTP_documentEmpty(MTP_long(fields.documentId)));
return;
}
state->generating = true;
crl::async([=] {
crl::on_main([=, ready = PackTheme(parsed)]{
if (!state->generating) {
return;
}
state->generating = false;
uploadFile(ready);
});
});
};
const auto checkFields = [=] {
state->requestId = api->request(MTPaccount_CreateTheme(
MTP_flags(MTPaccount_CreateTheme::Flag::f_document),
MTP_string(fields.slug),
MTP_string(fields.title),
MTP_inputDocumentEmpty(),
MTPVector<MTPInputThemeSettings>()
)).done([=](const MTPTheme &result) {
save();
}).fail([=](const MTP::Error &error) {
if (error.type() == u"THEME_FILE_INVALID"_q) {
save();
} else {
fail(SaveErrorType::Other, error.type());
}
}).send();
};
if (creating) {
checkFields();
} else {
save();
}
return [=] {
state->generating = false;
api->request(base::take(state->requestId)).cancel();
session->uploader().cancel(state->id);
state->lifetime.destroy();
};
}
} // namespace
bool PaletteChanged(
const QByteArray &editorPalette,
const Data::CloudTheme &cloud) {
auto object = Local::ReadThemeContent();
const auto real = object.content.isEmpty()
? GenerateDefaultPalette()
: ParseTheme(object, true).palette;
return PaletteChanged(editorPalette, real, cloud);
}
void StartEditor(
not_null<Window::Controller*> window,
const Data::CloudTheme &cloud) {
const auto path = EditingPalettePath();
auto object = Local::ReadThemeContent();
const auto palette = object.content.isEmpty()
? GenerateDefaultPalette()
: ParseTheme(object, true).palette;
if (palette.isEmpty() || !CopyColorsToPalette(path, palette, cloud)) {
window->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));
return;
}
if (Core::App().settings().systemDarkModeEnabled()) {
Core::App().settings().setSystemDarkModeEnabled(false);
Core::App().saveSettingsDelayed();
}
Background()->setEditingTheme(cloud);
window->showRightColumn(Box<Editor>(window, cloud));
}
void CreateBox(
not_null<Ui::GenericBox*> box,
not_null<Window::Controller*> window) {
CreateForExistingBox(box, window, Data::CloudTheme());
}
void CreateForExistingBox(
not_null<Ui::GenericBox*> box,
not_null<Window::Controller*> window,
const Data::CloudTheme &cloud) {
const auto amCreator = window->account().sessionExists()
&& (window->account().session().userId() == cloud.createdBy);
box->setTitle(amCreator
? (rpl::single(cloud.title) | Ui::Text::ToWithEntities())
: tr::lng_theme_editor_create_title(Ui::Text::WithEntities));
box->addRow(object_ptr<Ui::FlatLabel>(
box,
(amCreator
? tr::lng_theme_editor_attach_description
: tr::lng_theme_editor_create_description)(),
st::boxDividerLabel));
box->addRow(
object_ptr<Ui::SettingsButton>(
box,
tr::lng_theme_editor_import_existing() | Ui::Text::ToUpper(),
st::createThemeImportButton),
style::margins(
0,
st::boxRowPadding.left(),
0,
0)
)->addClickHandler([=] {
ImportFromFile(&window->account().session(), box);
});
const auto done = [=] {
box->closeBox();
StartEditor(window, cloud);
};
base::install_event_filter(box, box, [=](not_null<QEvent*> event) {
if (event->type() == QEvent::KeyPress) {
const auto key = static_cast<QKeyEvent*>(event.get())->key();
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
done();
return base::EventFilterResult::Cancel;
}
}
return base::EventFilterResult::Continue;
});
box->addButton(tr::lng_theme_editor_create(), done);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void SaveTheme(
not_null<Window::Controller*> window,
const Data::CloudTheme &cloud,
const QByteArray &palette,
Fn<void()> unlock) {
Expects(window->account().sessionExists());
using Data::CloudTheme;
const auto save = [=](const CloudTheme &fields) {
unlock();
window->show(Box(SaveThemeBox, window, fields, palette));
};
if (cloud.id) {
window->account().session().api().request(MTPaccount_GetTheme(
MTP_string(Data::CloudThemes::Format()),
MTP_inputTheme(MTP_long(cloud.id), MTP_long(cloud.accessHash))
)).done([=](const MTPTheme &result) {
result.match([&](const MTPDtheme &data) {
save(CloudTheme::Parse(&window->account().session(), data));
});
}).fail([=] {
save(CloudTheme());
}).send();
} else {
save(CloudTheme());
}
}
struct CollectedData {
QByteArray originalContent;
ParsedTheme originalParsed;
ParsedTheme parsed;
QImage background;
QColor accent;
};
[[nodiscard]] CollectedData CollectData(const QByteArray &palette) {
const auto original = Local::ReadThemeContent();
const auto originalContent = original.content;
// We don't need default palette here, because in case of it we are
// not interested if the palette was changed, we'll save it anyway.
const auto originalParsed = originalContent.isEmpty()
? ParsedTheme() // GenerateDefaultPalette()
: ParseTheme(original);
const auto background = Background()->createCurrentImage();
const auto changed = !Data::IsThemeWallPaper(Background()->paper())
|| originalParsed.background.isEmpty()
|| ColorizerForTheme(original.pathAbsolute);
auto parsed = ParsedTheme();
parsed.palette = StripCloudTextFields(palette);
parsed.isPng = false;
if (changed) {
QBuffer buffer(&parsed.background);
background.save(&buffer, "JPG", 87);
} else {
// Use existing background serialization.
parsed.background = originalParsed.background;
parsed.isPng = originalParsed.isPng;
}
const auto accent = st::windowActiveTextFg->c;
return { originalContent, originalParsed, parsed, background, accent };
}
QByteArray CollectForExport(const QByteArray &palette) {
return PackTheme(CollectData(palette).parsed);
}
void SaveThemeBox(
not_null<Ui::GenericBox*> box,
not_null<Window::Controller*> window,
const Data::CloudTheme &cloud,
const QByteArray &palette) {
Expects(window->account().sessionExists());
const auto collected = CollectData(palette);
const auto title = cloud.title.isEmpty()
? GenerateName(collected.accent)
: cloud.title;
box->setTitle(tr::lng_theme_editor_save_title(Ui::Text::WithEntities));
const auto name = box->addRow(object_ptr<Ui::InputField>(
box,
st::defaultInputField,
tr::lng_theme_editor_name(),
title));
const auto linkWrap = box->addRow(
object_ptr<Ui::RpWidget>(box),
style::margins(
st::boxRowPadding.left(),
st::themesSmallSkip,
st::boxRowPadding.right(),
st::boxRowPadding.bottom()));
const auto link = Ui::CreateChild<Ui::UsernameInput>(
linkWrap,
st::createThemeLink,
rpl::single(u"link"_q),
cloud.slug.isEmpty() ? GenerateSlug() : cloud.slug,
window->account().session().createInternalLink(QString()));
linkWrap->widthValue(
) | rpl::start_with_next([=](int width) {
link->resize(width, link->height());
link->moveToLeft(0, 0, width);
}, link->lifetime());
link->heightValue(
) | rpl::start_with_next([=](int height) {
linkWrap->resize(linkWrap->width(), height);
}, link->lifetime());
link->setLinkPlaceholder(
window->account().session().createInternalLink(u"addtheme/"_q));
link->setPlaceholderHidden(false);
link->setMaxLength(kMaxSlugSize);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_theme_editor_link_about(),
st::boxDividerLabel),
style::margins(
st::boxRowPadding.left(),
st::themesSmallSkip,
st::boxRowPadding.right(),
st::boxRowPadding.bottom()));
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_theme_editor_background_image(),
st::settingsSubsectionTitle),
st::settingsSubsectionTitlePadding);
const auto back = box->addRow(
object_ptr<BackgroundSelector>(
box,
collected.background,
collected.parsed),
style::margins(
st::boxRowPadding.left(),
st::themesSmallSkip,
st::boxRowPadding.right(),
st::boxRowPadding.bottom()));
box->setFocusCallback([=] { name->setFocusFast(); });
box->setWidth(st::boxWideWidth);
const auto saving = box->lifetime().make_state<bool>();
const auto cancel = std::make_shared<Fn<void()>>(nullptr);
box->lifetime().add([=] { if (*cancel) (*cancel)(); });
const auto save = [=] {
if (*saving) {
return;
}
const auto done = crl::guard(box, [=] {
box->closeBox();
window->showRightColumn(nullptr);
});
const auto fail = crl::guard(box, [=](
SaveErrorType type,
const QString &error) {
*saving = false;
box->showLoading(false);
if (error == u"THEME_TITLE_INVALID"_q) {
type = SaveErrorType::Name;
} else if (error == u"THEME_SLUG_INVALID"_q) {
type = SaveErrorType::Link;
} else if (error == u"THEME_SLUG_OCCUPIED"_q) {
box->showToast(
tr::lng_create_channel_link_occupied(tr::now));
type = SaveErrorType::Link;
} else if (!error.isEmpty()) {
box->showToast(error);
}
if (type == SaveErrorType::Name) {
name->showError();
} else if (type == SaveErrorType::Link) {
link->showError();
}
});
auto fields = cloud;
fields.title = name->getLastText().trimmed();
fields.slug = link->getLastText().trimmed();
if (fields.title.isEmpty()) {
fail(SaveErrorType::Name, QString());
return;
} else if (!IsGoodSlug(fields.slug)) {
fail(SaveErrorType::Link, QString());
return;
}
*saving = true;
box->showLoading(true);
*cancel = SavePreparedTheme(
window,
back->result(),
back->image(),
collected.originalContent,
collected.originalParsed,
fields,
done,
fail);
};
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
ParsedTheme ParseTheme(
const Object &theme,
bool onlyPalette,
bool parseCurrent) {
auto raw = ParsedTheme();
raw.palette = theme.content;
const auto result = [&] {
if (const auto colorizer = ColorizerForTheme(theme.pathAbsolute)) {
raw.palette = Editor::ColorizeInContent(
std::move(raw.palette),
colorizer);
}
if (parseCurrent) {
raw.palette = ReplaceAdjustableColors(std::move(raw.palette));
}
return raw;
};
zlib::FileToRead file(theme.content);
unz_global_info globalInfo = { 0 };
file.getGlobalInfo(&globalInfo);
if (file.error() != UNZ_OK) {
return result();
}
raw.palette = file.readFileContent("colors.tdesktop-theme", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);
if (file.error() == UNZ_END_OF_LIST_OF_FILE) {
file.clearError();
raw.palette = file.readFileContent("colors.tdesktop-palette", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);
}
if (file.error() != UNZ_OK) {
LOG(("Theme Error: could not read 'colors.tdesktop-theme' or 'colors.tdesktop-palette' in the theme file."));
return ParsedTheme();
} else if (onlyPalette) {
return result();
}
const auto fromFile = [&](const char *filename) {
raw.background = file.readFileContent(filename, zlib::kCaseInsensitive, kThemeBackgroundSizeLimit);
if (file.error() == UNZ_OK) {
return true;
} else if (file.error() == UNZ_END_OF_LIST_OF_FILE) {
file.clearError();
return true;
}
LOG(("Theme Error: could not read '%1' in the theme file.").arg(filename));
return false;
};
if (!fromFile("background.jpg") || !raw.background.isEmpty()) {
return raw.background.isEmpty() ? ParsedTheme() : result();
}
raw.isPng = true;
if (!fromFile("background.png") || !raw.background.isEmpty()) {
return raw.background.isEmpty() ? ParsedTheme() : result();
}
raw.tiled = true;
if (!fromFile("tiled.png") || !raw.background.isEmpty()) {
return raw.background.isEmpty() ? ParsedTheme() : result();
}
raw.isPng = false;
if (!fromFile("background.jpg") || !raw.background.isEmpty()) {
return raw.background.isEmpty() ? ParsedTheme() : result();
}
return result();
}
[[nodiscard]] QString GenerateSlug() {
const auto letters = uint8('Z' + 1 - 'A');
const auto digits = uint8('9' + 1 - '0');
const auto values = uint8(2 * letters + digits);
auto result = QString();
result.reserve(kRandomSlugSize);
for (auto i = 0; i != kRandomSlugSize; ++i) {
const auto value = base::RandomValue<uint8>() % values;
if (value < letters) {
result.append(char('A' + value));
} else if (value < 2 * letters) {
result.append(char('a' + (value - letters)));
} else {
result.append(char('0' + (value - 2 * letters)));
}
}
return result;
}
} // namespace Theme
} // namespace Window