diff --git a/Telegram/Resources/icons/theme_preview.png b/Telegram/Resources/icons/theme_preview.png new file mode 100644 index 0000000000..605ce5eb5f Binary files /dev/null and b/Telegram/Resources/icons/theme_preview.png differ diff --git a/Telegram/Resources/icons/theme_preview@2x.png b/Telegram/Resources/icons/theme_preview@2x.png new file mode 100644 index 0000000000..7a1868ddc4 Binary files /dev/null and b/Telegram/Resources/icons/theme_preview@2x.png differ diff --git a/Telegram/Resources/icons/theme_preview@3x.png b/Telegram/Resources/icons/theme_preview@3x.png new file mode 100644 index 0000000000..6090ba7ca4 Binary files /dev/null and b/Telegram/Resources/icons/theme_preview@3x.png differ diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp index a754953803..3bb20a0b4c 100644 --- a/Telegram/SourceFiles/storage/localstorage.cpp +++ b/Telegram/SourceFiles/storage/localstorage.cpp @@ -34,7 +34,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "window/themes/window_theme.h" #include "window/window_session_controller.h" -#include "window/themes/window_theme_editor.h" #include "base/flags.h" #include "data/data_session.h" #include "history/history.h" @@ -4479,26 +4478,30 @@ std::vector readRecentLanguages() { return result; } -bool copyThemeColorsToPalette(const QString &destination) { +Window::Theme::Object ReadThemeContent() { using namespace Window::Theme; + auto &themeKey = IsNightMode() ? _themeKeyNight : _themeKeyDay; if (!themeKey) { - return false; + return Object(); } FileReadDescriptor theme; if (!readEncryptedFile(theme, themeKey, FileOption::Safe, SettingsKey)) { - return false; + return Object(); } - QByteArray themeContent; + QByteArray content; QString pathRelative, pathAbsolute; - theme.stream >> themeContent >> pathRelative >> pathAbsolute; + theme.stream >> content >> pathRelative >> pathAbsolute; if (theme.stream.status() != QDataStream::Ok) { - return false; + return Object(); } - return CopyColorsToPalette(destination, pathAbsolute, themeContent); + auto result = Object(); + result.pathAbsolute = pathAbsolute; + result.content = content; + return result; } void writeRecentHashtagsAndBots() { diff --git a/Telegram/SourceFiles/storage/localstorage.h b/Telegram/SourceFiles/storage/localstorage.h index 462ea51dc5..6d7641ca01 100644 --- a/Telegram/SourceFiles/storage/localstorage.h +++ b/Telegram/SourceFiles/storage/localstorage.h @@ -26,6 +26,7 @@ class EncryptionKey; namespace Window { namespace Theme { +struct Object; struct Saved; } // namespace Theme } // namespace Window @@ -152,8 +153,9 @@ bool readBackground(); void writeTheme(const Window::Theme::Saved &saved); void clearTheme(); -bool copyThemeColorsToPalette(const QString &destination); -Window::Theme::Saved readThemeAfterSwitch(); +[[nodiscard]] Window::Theme::Saved readThemeAfterSwitch(); + +[[nodiscard]] Window::Theme::Object ReadThemeContent(); void writeLangPack(); void pushRecentLanguage(const Lang::Language &language); diff --git a/Telegram/SourceFiles/window/themes/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp index 11a0306356..26a3c7f2af 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/themes/window_theme_preview.h" #include "window/themes/window_themes_embedded.h" +#include "window/themes/window_theme_editor.h" #include "mainwidget.h" #include "main/main_session.h" #include "apiwrap.h" @@ -49,14 +50,6 @@ Applying GlobalApplying; inline bool AreTestingTheme() { return !GlobalApplying.paletteForRevert.isEmpty(); -}; - -[[nodiscard]] bool IsEditingTheme(const QString &path) { - static const auto kEditingPath = QFileInfo( - EditingPalettePath() - ).absoluteFilePath(); - return !path.compare(kEditingPath, Qt::CaseInsensitive) - && QFileInfo(path).exists(); } bool CalculateIsMonoColorImage(const QImage &image) { @@ -408,7 +401,7 @@ bool InitializeFromSaved(Saved &&saved) { return false; } if (editing) { - Background()->setIsEditingTheme(true); + Background()->setEditingTheme(ReadCloudFromText(*editing)); } else { Local::writeTheme(saved); } @@ -544,7 +537,7 @@ void ChatBackground::checkUploadWallPaper() { } if (!Data::IsCustomWallPaper(_paper) || _original.isNull() - || isEditingTheme()) { + || _editingTheme.has_value()) { return; } @@ -734,7 +727,7 @@ bool ChatBackground::adjustPaletteRequired() { || Data::details::IsTestingDefaultWallPaper(_paper); }; - if (isEditingTheme()) { + if (_editingTheme.has_value()) { return false; } else if (isNonDefaultThemeOrBackground() || nightMode()) { return !usingThemeBackground(); @@ -742,12 +735,13 @@ bool ChatBackground::adjustPaletteRequired() { return !usingDefaultBackground(); } -bool ChatBackground::isEditingTheme() const { +std::optional ChatBackground::editingTheme() const { return _editingTheme; } -void ChatBackground::setIsEditingTheme(bool editing) { - if (_editingTheme == editing) { +void ChatBackground::setEditingTheme( + std::optional editing) { + if (!_editingTheme && !editing) { return; } _editingTheme = editing; @@ -913,7 +907,7 @@ void ChatBackground::setTestingTheme(Instance &&theme) { || (Data::IsDefaultWallPaper(_paper) && !nightMode() && _themeObject.pathAbsolute.isEmpty()); - if (AreTestingTheme() && isEditingTheme()) { + if (AreTestingTheme() && _editingTheme.has_value()) { // Grab current background image if it is not already custom // Use prepared pixmap, not original image, because we're // for sure switching to a non-pattern wall-paper (testing editor). diff --git a/Telegram/SourceFiles/window/themes/window_theme.h b/Telegram/SourceFiles/window/themes/window_theme.h index 75ee9dee24..fbbe85226d 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.h +++ b/Telegram/SourceFiles/window/themes/window_theme.h @@ -120,8 +120,8 @@ public: void setTileNightValue(bool tile); void setThemeObject(const Object &object); [[nodiscard]] const Object &themeObject() const; - [[nodiscard]] bool isEditingTheme() const; - void setIsEditingTheme(bool editing); + [[nodiscard]] std::optional editingTheme() const; + void setEditingTheme(std::optional editing); void reset(); void setTestingTheme(Instance &&theme); @@ -201,7 +201,7 @@ private: Object _themeObject; QImage _themeImage; bool _themeTile = false; - bool _editingTheme = false; + std::optional _editingTheme; Data::WallPaper _paperForRevert = Data::details::UninitializedWallPaper(); @@ -221,11 +221,6 @@ ChatBackground *Background(); void ComputeBackgroundRects(QRect wholeFill, QSize imageSize, QRect &to, QRect &from); -bool CopyColorsToPalette( - const QString &destination, - const QString &themePath, - const QByteArray &themeContent); - bool ReadPaletteValues(const QByteArray &content, Fn callback); } // namespace Theme diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor.cpp b/Telegram/SourceFiles/window/themes/window_theme_editor.cpp index 675f54058d..2e4ecdaae7 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_editor.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_editor.cpp @@ -35,6 +35,14 @@ namespace Window { namespace Theme { namespace { +template +QByteArray qba(const char(&string)[Size]) { + return QByteArray::fromRawData(string, Size - 1); +} + +const auto kCloudInTextStart = qba("// THEME EDITOR SERVICE INFO START\n"); +const auto kCloudInTextEnd = qba("// THEME EDITOR SERVICE INFO END\n\n"); + struct ReadColorResult { ReadColorResult(QColor color, bool error = false) : color(color), error(error) { } @@ -186,7 +194,7 @@ QByteArray replaceValueInContent(const QByteArray &content, const QByteArray &na return QByteArray(); } -QByteArray ColorizeInContent( +[[nodiscard]] QByteArray ColorizeInContent( QByteArray content, const Colorizer &colorizer) { auto validNames = OrderedSet(); @@ -294,44 +302,41 @@ private: }; -bool CopyColorsToPalette( - const QString &destination, - const QString &themePath, - const QByteArray &themeContent) { - auto paletteContent = themeContent; +[[nodiscard]] QByteArray WriteCloudToText(const Data::CloudTheme &cloud) { + auto result = QByteArray(); + const auto add = [&](const QByteArray &key, const QString &value) { + result.append("// " + key + ": " + value.toLatin1() + "\n"); + }; + result.append(kCloudInTextStart); + add("ID", QString::number(cloud.id)); + add("ACCESS", QString::number(cloud.accessHash)); + result.append(kCloudInTextEnd); + return result; +} - zlib::FileToRead file(themeContent); - - unz_global_info globalInfo = { 0 }; - file.getGlobalInfo(&globalInfo); - if (file.error() == UNZ_OK) { - paletteContent = file.readFileContent("colors.tdesktop-theme", zlib::kCaseInsensitive, kThemeSchemeSizeLimit); - if (file.error() == UNZ_END_OF_LIST_OF_FILE) { - file.clearError(); - paletteContent = 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, while copying to '%1'.").arg(destination)); +[[nodiscard]] Data::CloudTheme ReadCloudFromText(const QByteArray &text) { + const auto index = text.indexOf(kCloudInTextEnd); + if (index <= 1) { + return Data::CloudTheme(); + } + auto result = Data::CloudTheme(); + const auto list = text.mid(0, index - 1).split('\n'); + const auto take = [&](uint64 &value, int index) { + if (list.size() <= index) { return false; } + const auto &entry = list[index]; + const auto position = entry.indexOf(": "); + if (position < 0) { + return false; + } + value = QString::fromLatin1(entry.mid(position + 2)).toULongLong(); + return true; + }; + if (!take(result.id, 1) || !take(result.accessHash, 2)) { + return Data::CloudTheme(); } - - QFile f(destination); - if (!f.open(QIODevice::WriteOnly)) { - LOG(("Theme Error: could not open file for write '%1'").arg(destination)); - return false; - } - - if (const auto colorizer = ColorizerForTheme(themePath)) { - paletteContent = ColorizeInContent( - std::move(paletteContent), - colorizer); - } - if (f.write(paletteContent) != paletteContent.size()) { - LOG(("Theme Error: could not write palette to '%1'").arg(destination)); - return false; - } - return true; + return result; } Editor::Inner::Inner(QWidget *parent, const QString &path) : TWidget(parent) @@ -680,12 +685,22 @@ Editor::Editor( resizeToWidth(st::windowMinWidth); } +QByteArray Editor::ColorizeInContent( + QByteArray content, + const Colorizer &colorizer) { + return Window::Theme::ColorizeInContent(content, colorizer); +} + void Editor::save() { if (!_window->account().sessionExists()) { Ui::Toast::Show(tr::lng_theme_editor_need_auth(tr::now)); return; + } else if (_saving) { + return; } - Ui::show(Box(SaveThemeBox, _window, _cloud, _inner->paletteContent())); + _saving = true; + const auto unlock = crl::guard(this, [=] { _saving = false; }); + SaveTheme(_window, _cloud, _inner->paletteContent(), unlock); } void Editor::resizeEvent(QResizeEvent *e) { @@ -776,7 +791,7 @@ void Editor::paintEvent(QPaintEvent *e) { void Editor::closeEditor() { if (const auto window = App::wnd()) { window->showRightColumn(nullptr); - Background()->setIsEditingTheme(false); + Background()->setEditingTheme(std::nullopt); } } diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor.h b/Telegram/SourceFiles/window/themes/window_theme_editor.h index 84ec3ef5bf..db8e945307 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_editor.h +++ b/Telegram/SourceFiles/window/themes/window_theme_editor.h @@ -23,10 +23,10 @@ class Controller; namespace Theme { -bool CopyColorsToPalette( - const QString &destination, - const QString &themePath, - const QByteArray &themeContent); +struct Colorizer; + +[[nodiscard]] QByteArray WriteCloudToText(const Data::CloudTheme &cloud); +[[nodiscard]] Data::CloudTheme ReadCloudFromText(const QByteArray &text); class Editor : public TWidget { public: @@ -35,6 +35,10 @@ public: not_null window, const Data::CloudTheme &cloud); + [[nodiscard]] static QByteArray ColorizeInContent( + QByteArray content, + const Colorizer &colorizer); + protected: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; @@ -57,6 +61,7 @@ private: object_ptr _leftShadow; object_ptr _topShadow; object_ptr _save; + bool _saving = false; }; diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp b/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp index 4aa85fa3b1..bf0566bac0 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp @@ -86,6 +86,7 @@ private: QByteArray _backgroundContent; bool _isPng = false; QString _imageText; + int _thumbnailSize = 0; QPixmap _thumbnail; }; @@ -112,12 +113,12 @@ BackgroundSelector::BackgroundSelector( formatSizeText(_backgroundContent.size())); _chooseFromFile->setClickedCallback([=] { chooseBackgroundFromFile(); }); - const auto height = st::boxTextFont->height + _thumbnailSize = st::boxTextFont->height + st::themesSmallSkip + _chooseFromFile->heightNoMargins() + st::themesSmallSkip + _tileBackground->heightNoMargins(); - resize(width(), height); + resize(width(), _thumbnailSize + st::themesSmallSkip); updateThumbnail(); } @@ -125,7 +126,7 @@ BackgroundSelector::BackgroundSelector( void BackgroundSelector::paintEvent(QPaintEvent *e) { Painter p(this); - const auto left = height() + st::themesSmallSkip; + const auto left = _thumbnailSize + st::themesSmallSkip; p.setPen(st::boxTextFg); p.setFont(st::boxTextFont); @@ -135,14 +136,14 @@ void BackgroundSelector::paintEvent(QPaintEvent *e) { } int BackgroundSelector::resizeGetHeight(int newWidth) { - const auto left = height() + st::themesSmallSkip; + 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 = height(); + const auto size = _thumbnailSize; auto back = QImage( QSize(size, size) * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); @@ -233,11 +234,55 @@ void ImportFromFile( crl::guard(parent, callback)); } -QString BytesToUTF8(QLatin1String string) { +[[nodiscard]] QString BytesToUTF8(QLatin1String string) { return QString::fromUtf8(string.data(), string.size()); } -bool WriteDefaultPalette(const QString &path) { +[[nodiscard]] bool CopyColorsToPalette( + const QString &destination, + const QString &themePath, + const QByteArray &themeContent, + const Data::CloudTheme &cloud) { + auto paletteContent = themeContent; + + zlib::FileToRead file(themeContent); + + unz_global_info globalInfo = { 0 }; + file.getGlobalInfo(&globalInfo); + if (file.error() == UNZ_OK) { + paletteContent = file.readFileContent("colors.tdesktop-theme", zlib::kCaseInsensitive, kThemeSchemeSizeLimit); + if (file.error() == UNZ_END_OF_LIST_OF_FILE) { + file.clearError(); + paletteContent = 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, while copying to '%1'.").arg(destination)); + return false; + } + } + + QFile f(destination); + if (!f.open(QIODevice::WriteOnly)) { + LOG(("Theme Error: could not open file for write '%1'").arg(destination)); + return false; + } + + if (const auto colorizer = ColorizerForTheme(themePath)) { + paletteContent = Editor::ColorizeInContent( + std::move(paletteContent), + colorizer); + } + paletteContent = WriteCloudToText(cloud) + paletteContent; + if (f.write(paletteContent) != paletteContent.size()) { + LOG(("Theme Error: could not write palette to '%1'").arg(destination)); + return false; + } + return true; +} + +bool WriteDefaultPalette( + const QString &path, + const Data::CloudTheme &cloud) { QFile f(path); if (!f.open(QIODevice::WriteOnly)) { LOG(("Theme Error: could not open '%1' for writing.").arg(path)); @@ -247,6 +292,8 @@ bool WriteDefaultPalette(const QString &path) { QTextStream stream(&f); stream.setCodec("UTF-8"); + stream << QString::fromLatin1(WriteCloudToText(cloud)); + auto rows = style::main_palette::data(); for (const auto &row : std::as_const(rows)) { stream @@ -396,7 +443,7 @@ SendMediaReady PrepareThemeMedia( 0); } -Fn SaveTheme( +Fn SavePreparedTheme( not_null window, const QByteArray &palette, const PreparedBackground &background, @@ -494,12 +541,15 @@ void StartEditor( not_null window, const Data::CloudTheme &cloud) { const auto path = EditingPalettePath(); - if (!Local::copyThemeColorsToPalette(path) - && !WriteDefaultPalette(path)) { + auto object = Local::ReadThemeContent(); + const auto written = object.content.isEmpty() + ? WriteDefaultPalette(path, cloud) + : CopyColorsToPalette(path, object.pathAbsolute, object.content, cloud); + if (!written) { window->show(Box(tr::lng_theme_editor_error(tr::now))); return; } - Background()->setIsEditingTheme(true); + Background()->setEditingTheme(cloud); window->showRightColumn(Box(window, cloud)); } @@ -561,6 +611,40 @@ void CreateForExistingBox( box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } +void SaveTheme( + not_null window, + const Data::CloudTheme &cloud, + const QByteArray &palette, + Fn unlock) { + Expects(window->account().sessionExists()); + + using Data::CloudTheme; + + const auto save = [=](const CloudTheme &fields) { + 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)), + MTP_long(0) + )).done([=](const MTPTheme &result) { + unlock(); + result.match([&](const MTPDtheme &data) { + save(CloudTheme::Parse(&window->account().session(), data)); + }, [&](const MTPDthemeDocumentNotModified &data) { + LOG(("API Error: Unexpected themeDocumentNotModified.")); + save(CloudTheme()); + }); + }).fail([=](const RPCError &error) { + unlock(); + save(CloudTheme()); + }).send(); + } else { + save(CloudTheme()); + } +} + void SaveThemeBox( not_null box, not_null window, @@ -594,7 +678,7 @@ void SaveThemeBox( linkWrap, st::createThemeLink, rpl::single(qsl("link")), - cloud.slug, + cloud.slug.isEmpty() ? GenerateSlug() : cloud.slug, true); linkWrap->widthValue( ) | rpl::start_with_next([=](int width) { @@ -657,6 +741,7 @@ void SaveThemeBox( const auto fail = crl::guard(box, [=]( SaveErrorType type, const QString &text) { + *saving = false; if (!text.isEmpty()) { Ui::Toast::Show(text); } @@ -666,7 +751,7 @@ void SaveThemeBox( link->showError(); } }); - *cancel = SaveTheme( + *cancel = SavePreparedTheme( window, palette, back->result(), diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor_box.h b/Telegram/SourceFiles/window/themes/window_theme_editor_box.h index 158da4575c..1280822010 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_editor_box.h +++ b/Telegram/SourceFiles/window/themes/window_theme_editor_box.h @@ -29,6 +29,11 @@ void CreateForExistingBox( not_null box, not_null window, const Data::CloudTheme &cloud); +void SaveTheme( + not_null window, + const Data::CloudTheme &cloud, + const QByteArray &palette, + Fn unlock); void SaveThemeBox( not_null box, not_null window, diff --git a/Telegram/SourceFiles/window/window_controller.cpp b/Telegram/SourceFiles/window/window_controller.cpp index 0b6372612b..93d89dccd3 100644 --- a/Telegram/SourceFiles/window/window_controller.cpp +++ b/Telegram/SourceFiles/window/window_controller.cpp @@ -46,9 +46,9 @@ void Controller::firstShow() { void Controller::checkThemeEditor() { using namespace Window::Theme; - if (Background()->isEditingTheme()) { - showRightColumn( - Box(this, Background()->themeObject().cloud)); + + if (const auto editing = Background()->editingTheme()) { + showRightColumn(Box(this, *editing)); } }