window_theme_editor_box.cpp 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "window/themes/window_theme_editor_box.h"
  8. #include "window/themes/window_theme.h"
  9. #include "window/themes/window_theme_editor.h"
  10. #include "window/themes/window_theme_preview.h"
  11. #include "window/themes/window_themes_generate_name.h"
  12. #include "window/window_controller.h"
  13. #include "ui/boxes/confirm_box.h"
  14. #include "ui/text/text_utilities.h"
  15. #include "ui/widgets/fields/input_field.h"
  16. #include "ui/widgets/checkbox.h"
  17. #include "ui/widgets/buttons.h"
  18. #include "ui/widgets/labels.h"
  19. #include "ui/image/image_prepare.h"
  20. #include "ui/toast/toast.h"
  21. #include "ui/text/format_values.h"
  22. #include "ui/style/style_palette_colorizer.h"
  23. #include "ui/widgets/fields/special_fields.h"
  24. #include "ui/painter.h"
  25. #include "ui/ui_utility.h"
  26. #include "main/main_account.h"
  27. #include "main/main_session.h"
  28. #include "storage/localstorage.h"
  29. #include "core/file_utilities.h"
  30. #include "core/application.h"
  31. #include "lang/lang_keys.h"
  32. #include "base/event_filter.h"
  33. #include "base/base_file_utilities.h"
  34. #include "base/zlib_help.h"
  35. #include "base/unixtime.h"
  36. #include "base/random.h"
  37. #include "data/data_session.h"
  38. #include "data/data_document.h"
  39. #include "data/data_cloud_themes.h"
  40. #include "storage/file_upload.h"
  41. #include "mainwindow.h"
  42. #include "apiwrap.h"
  43. #include "styles/style_widgets.h"
  44. #include "styles/style_window.h"
  45. #include "styles/style_layers.h"
  46. #include "styles/style_boxes.h"
  47. #include <QtCore/QBuffer>
  48. namespace Window {
  49. namespace Theme {
  50. namespace {
  51. constexpr auto kRandomSlugSize = 16;
  52. constexpr auto kMinSlugSize = 5;
  53. constexpr auto kMaxSlugSize = 64;
  54. enum class SaveErrorType {
  55. Other,
  56. Name,
  57. Link,
  58. };
  59. class BackgroundSelector : public Ui::RpWidget {
  60. public:
  61. BackgroundSelector(
  62. QWidget *parent,
  63. const QImage &background,
  64. const ParsedTheme &parsed);
  65. [[nodiscard]] ParsedTheme result() const;
  66. [[nodiscard]] QImage image() const;
  67. int resizeGetHeight(int newWidth) override;
  68. protected:
  69. void paintEvent(QPaintEvent *e) override;
  70. private:
  71. void updateThumbnail();
  72. void chooseBackgroundFromFile();
  73. object_ptr<Ui::LinkButton> _chooseFromFile;
  74. object_ptr<Ui::Checkbox> _tileBackground;
  75. QImage _background;
  76. ParsedTheme _parsed;
  77. QString _imageText;
  78. int _thumbnailSize = 0;
  79. QPixmap _thumbnail;
  80. };
  81. template <size_t Size>
  82. QByteArray qba(const char(&string)[Size]) {
  83. return QByteArray::fromRawData(string, Size - 1);
  84. }
  85. QByteArray qba(QLatin1String string) {
  86. return QByteArray::fromRawData(string.data(), string.size());
  87. }
  88. BackgroundSelector::BackgroundSelector(
  89. QWidget *parent,
  90. const QImage &background,
  91. const ParsedTheme &parsed)
  92. : RpWidget(parent)
  93. , _chooseFromFile(
  94. this,
  95. tr::lng_settings_bg_from_file(tr::now),
  96. st::boxLinkButton)
  97. , _tileBackground(
  98. this,
  99. tr::lng_settings_bg_tile(tr::now),
  100. parsed.tiled,
  101. st::defaultBoxCheckbox)
  102. , _background(background)
  103. , _parsed(parsed) {
  104. _imageText = tr::lng_theme_editor_saved_to_jpg(
  105. tr::now,
  106. lt_size,
  107. Ui::FormatSizeText(_parsed.background.size()));
  108. _chooseFromFile->setClickedCallback([=] { chooseBackgroundFromFile(); });
  109. _thumbnailSize = st::boxTextFont->height
  110. + st::themesSmallSkip
  111. + _chooseFromFile->heightNoMargins()
  112. + st::themesSmallSkip
  113. + _tileBackground->heightNoMargins();
  114. resize(width(), _thumbnailSize + st::themesSmallSkip);
  115. updateThumbnail();
  116. }
  117. void BackgroundSelector::paintEvent(QPaintEvent *e) {
  118. Painter p(this);
  119. const auto left = _thumbnailSize + st::themesSmallSkip;
  120. p.setPen(st::boxTextFg);
  121. p.setFont(st::boxTextFont);
  122. p.drawTextLeft(left, 0, width(), _imageText);
  123. p.drawPixmapLeft(0, 0, width(), _thumbnail);
  124. }
  125. int BackgroundSelector::resizeGetHeight(int newWidth) {
  126. const auto left = _thumbnailSize + st::themesSmallSkip;
  127. _chooseFromFile->moveToLeft(left, st::boxTextFont->height + st::themesSmallSkip);
  128. _tileBackground->moveToLeft(left, st::boxTextFont->height + st::themesSmallSkip + _chooseFromFile->height() + st::themesSmallSkip);
  129. return height();
  130. }
  131. void BackgroundSelector::updateThumbnail() {
  132. const auto size = _thumbnailSize;
  133. auto back = QImage(
  134. QSize(size, size) * style::DevicePixelRatio(),
  135. QImage::Format_ARGB32_Premultiplied);
  136. back.setDevicePixelRatio(style::DevicePixelRatio());
  137. {
  138. Painter p(&back);
  139. PainterHighQualityEnabler hq(p);
  140. auto &pix = _background;
  141. int sx = (pix.width() > pix.height()) ? ((pix.width() - pix.height()) / 2) : 0;
  142. int sy = (pix.height() > pix.width()) ? ((pix.height() - pix.width()) / 2) : 0;
  143. int s = (pix.width() > pix.height()) ? pix.height() : pix.width();
  144. p.drawImage(QRect(0, 0, size, size), pix, QRect(sx, sy, s, s));
  145. }
  146. _thumbnail = Ui::PixmapFromImage(
  147. Images::Round(std::move(back), ImageRoundRadius::Small));
  148. _thumbnail.setDevicePixelRatio(style::DevicePixelRatio());
  149. update();
  150. }
  151. void BackgroundSelector::chooseBackgroundFromFile() {
  152. const auto callback = [=](const FileDialog::OpenResult &result) {
  153. auto content = result.remoteContent;
  154. if (!result.paths.isEmpty()) {
  155. QFile f(result.paths.front());
  156. if (f.open(QIODevice::ReadOnly)) {
  157. content = f.readAll();
  158. f.close();
  159. }
  160. }
  161. if (!content.isEmpty()) {
  162. auto read = Images::Read({ .content = content });
  163. if (!read.image.isNull()
  164. && (read.format == "jpeg"
  165. || read.format == "jpg"
  166. || read.format == "png")) {
  167. _background = std::move(read.image);
  168. _parsed.background = content;
  169. _parsed.isPng = (read.format == "png");
  170. const auto phrase = _parsed.isPng
  171. ? tr::lng_theme_editor_read_from_png
  172. : tr::lng_theme_editor_read_from_jpg;
  173. _imageText = phrase(
  174. tr::now,
  175. lt_size,
  176. Ui::FormatSizeText(_parsed.background.size()));
  177. _tileBackground->setChecked(false);
  178. updateThumbnail();
  179. }
  180. }
  181. };
  182. FileDialog::GetOpenPath(
  183. this,
  184. tr::lng_theme_editor_choose_image(tr::now),
  185. "Image files (*.jpeg *.jpg *.png)",
  186. crl::guard(this, callback));
  187. }
  188. ParsedTheme BackgroundSelector::result() const {
  189. auto result = _parsed;
  190. result.tiled = _tileBackground->checked();
  191. return result;
  192. }
  193. QImage BackgroundSelector::image() const {
  194. return _background;
  195. }
  196. bool PaletteChanged(
  197. const QByteArray &editorPalette,
  198. const QByteArray &originalPalette,
  199. const Data::CloudTheme &cloud) {
  200. return originalPalette.isEmpty()
  201. || (editorPalette != WriteCloudToText(cloud) + originalPalette);
  202. }
  203. void ImportFromFile(
  204. not_null<Main::Session*> session,
  205. not_null<QWidget*> parent) {
  206. auto filters = QStringList(
  207. u"Theme files (*.tdesktop-theme *.tdesktop-palette)"_q);
  208. filters.push_back(FileDialog::AllFilesFilter());
  209. const auto callback = crl::guard(session, [=](
  210. const FileDialog::OpenResult &result) {
  211. const auto path = result.paths.isEmpty()
  212. ? QString()
  213. : result.paths.front();
  214. if (!path.isEmpty()) {
  215. Window::Theme::Apply(path);
  216. }
  217. });
  218. FileDialog::GetOpenPath(
  219. parent.get(),
  220. tr::lng_theme_editor_menu_import(tr::now),
  221. filters.join(u";;"_q),
  222. crl::guard(parent, callback));
  223. }
  224. // They're duplicated in window_theme.cpp:ChatBackground::ChatBackground.
  225. [[nodiscard]] QByteArray ReplaceAdjustableColors(QByteArray data) {
  226. const auto &themeObject = Background()->themeObject();
  227. const auto &paper = Background()->paper();
  228. const auto usingDefaultTheme = themeObject.pathAbsolute.isEmpty();
  229. const auto usingThemeBackground = usingDefaultTheme
  230. ? Data::IsDefaultWallPaper(paper)
  231. : Data::IsThemeWallPaper(paper);
  232. if (usingThemeBackground) {
  233. return data;
  234. }
  235. const auto adjustables = base::flat_map<QByteArray, style::color>{
  236. { qba(qstr("msgServiceBg")), st::msgServiceBg },
  237. { qba(qstr("msgServiceBgSelected")), st::msgServiceBgSelected },
  238. { qba(qstr("historyScrollBg")), st::historyScrollBg },
  239. { qba(qstr("historyScrollBgOver")), st::historyScrollBgOver },
  240. { qba(qstr("historyScrollBarBg")), st::historyScrollBarBg },
  241. { qba(qstr("historyScrollBarBgOver")), st::historyScrollBarBgOver }
  242. };
  243. for (const auto &[name, color] : adjustables) {
  244. data = ReplaceValueInPaletteContent(
  245. data,
  246. name,
  247. ColorHexString(color->c));
  248. if (data == "error") {
  249. LOG(("Theme Error: could not adjust '%1: %2' in content").arg(
  250. QString::fromLatin1(name),
  251. QString::fromLatin1(ColorHexString(color->c))));
  252. return QByteArray();
  253. }
  254. }
  255. return data;
  256. }
  257. QByteArray GenerateDefaultPalette() {
  258. auto result = QByteArray();
  259. const auto rows = style::main_palette::data();
  260. for (const auto &row : std::as_const(rows)) {
  261. result.append(qba(row.name)
  262. ).append(": "
  263. ).append(qba(row.value)
  264. ).append("; // "
  265. ).append(
  266. qba(
  267. row.description
  268. ).replace(
  269. '\n',
  270. ' '
  271. ).replace(
  272. '\r',
  273. ' ')
  274. ).append('\n');
  275. }
  276. return result;
  277. }
  278. bool CopyColorsToPalette(
  279. const QString &path,
  280. const QByteArray &palette,
  281. const Data::CloudTheme &cloud) {
  282. QFile f(path);
  283. if (!f.open(QIODevice::WriteOnly)) {
  284. LOG(("Theme Error: could not open '%1' for writing.").arg(path));
  285. return false;
  286. }
  287. const auto prefix = WriteCloudToText(cloud);
  288. if (f.write(prefix) != prefix.size()
  289. || f.write(palette) != palette.size()) {
  290. LOG(("Theme Error: could not write palette to '%1'").arg(path));
  291. return false;
  292. }
  293. return true;
  294. }
  295. [[nodiscard]] QByteArray PackTheme(const ParsedTheme &parsed) {
  296. zlib::FileToWrite zip;
  297. zip_fileinfo zfi = { { 0, 0, 0, 0, 0, 0 }, 0, 0, 0 };
  298. const auto back = std::string(parsed.tiled ? "tiled" : "background")
  299. + (parsed.isPng ? ".png" : ".jpg");
  300. zip.openNewFile(
  301. back.c_str(),
  302. &zfi,
  303. nullptr,
  304. 0,
  305. nullptr,
  306. 0,
  307. nullptr,
  308. Z_DEFLATED,
  309. Z_DEFAULT_COMPRESSION);
  310. zip.writeInFile(
  311. parsed.background.constData(),
  312. parsed.background.size());
  313. zip.closeFile();
  314. const auto scheme = "colors.tdesktop-theme";
  315. zip.openNewFile(
  316. scheme,
  317. &zfi,
  318. nullptr,
  319. 0,
  320. nullptr,
  321. 0,
  322. nullptr,
  323. Z_DEFLATED,
  324. Z_DEFAULT_COMPRESSION);
  325. zip.writeInFile(parsed.palette.constData(), parsed.palette.size());
  326. zip.closeFile();
  327. zip.close();
  328. if (zip.error() != ZIP_OK) {
  329. LOG(("Theme Error: could not export zip-ed theme, status: %1"
  330. ).arg(zip.error()));
  331. return QByteArray();
  332. }
  333. return zip.result();
  334. }
  335. [[nodiscard]] bool IsGoodSlug(const QString &slug) {
  336. if (slug.size() < kMinSlugSize || slug.size() > kMaxSlugSize) {
  337. return false;
  338. }
  339. return ranges::none_of(slug, [](QChar ch) {
  340. return (ch < 'A' || ch > 'Z')
  341. && (ch < 'a' || ch > 'z')
  342. && (ch < '0' || ch > '9')
  343. && (ch != '_');
  344. });
  345. }
  346. std::shared_ptr<FilePrepareResult> PrepareThemeMedia(
  347. MTP::DcId dcId,
  348. const QString &name,
  349. const QByteArray &content) {
  350. PreparedPhotoThumbs thumbnails;
  351. QVector<MTPPhotoSize> sizes;
  352. auto thumbnail = GeneratePreview(content, QString()).scaled(
  353. 320,
  354. 320,
  355. Qt::KeepAspectRatio,
  356. Qt::SmoothTransformation);
  357. auto thumbnailBytes = QByteArray();
  358. {
  359. QBuffer buffer(&thumbnailBytes);
  360. thumbnail.save(&buffer, "JPG", 87);
  361. }
  362. sizes.push_back(MTP_photoSize(
  363. MTP_string("s"),
  364. MTP_int(thumbnail.width()),
  365. MTP_int(thumbnail.height()), MTP_int(0)));
  366. const auto id = base::RandomValue<DocumentId>();
  367. const auto filename = base::FileNameFromUserString(name)
  368. + u".tdesktop-theme"_q;
  369. auto attributes = QVector<MTPDocumentAttribute>(
  370. 1,
  371. MTP_documentAttributeFilename(MTP_string(filename)));
  372. auto result = MakePreparedFile({
  373. .id = id,
  374. .type = SendMediaType::ThemeFile,
  375. });
  376. result->filename = filename;
  377. result->content = content;
  378. result->filesize = content.size();
  379. result->thumb = thumbnail;
  380. result->thumbname = "thumb.jpg";
  381. result->setThumbData(thumbnailBytes);
  382. result->document = MTP_document(
  383. MTP_flags(0),
  384. MTP_long(id),
  385. MTP_long(0),
  386. MTP_bytes(),
  387. MTP_int(base::unixtime::now()),
  388. MTP_string("application/x-tgtheme-tdesktop"),
  389. MTP_long(content.size()),
  390. MTP_vector<MTPPhotoSize>(sizes),
  391. MTPVector<MTPVideoSize>(),
  392. MTP_int(dcId),
  393. MTP_vector<MTPDocumentAttribute>(attributes));
  394. return result;
  395. }
  396. Fn<void()> SavePreparedTheme(
  397. not_null<Window::Controller*> window,
  398. const ParsedTheme &parsed,
  399. const QImage &background,
  400. const QByteArray &originalContent,
  401. const ParsedTheme &originalParsed,
  402. const Data::CloudTheme &fields,
  403. Fn<void()> done,
  404. Fn<void(SaveErrorType,QString)> fail) {
  405. Expects(window->account().sessionExists());
  406. using Storage::UploadedMedia;
  407. struct State {
  408. FullMsgId id;
  409. bool generating = false;
  410. mtpRequestId requestId = 0;
  411. QByteArray themeContent;
  412. QString filename;
  413. rpl::lifetime lifetime;
  414. };
  415. const auto session = &window->account().session();
  416. const auto api = &session->api();
  417. const auto state = std::make_shared<State>();
  418. state->id = FullMsgId(
  419. session->userPeerId(),
  420. session->data().nextLocalMessageId());
  421. const auto creating = !fields.id
  422. || (fields.createdBy != session->userId());
  423. const auto changed = (parsed.background != originalParsed.background)
  424. || (parsed.tiled != originalParsed.tiled)
  425. || PaletteChanged(parsed.palette, originalParsed.palette, fields);
  426. const auto finish = [=](const MTPTheme &result) {
  427. Background()->clearEditingTheme(ClearEditing::KeepChanges);
  428. done();
  429. const auto cloud = result.match([&](const MTPDtheme &data) {
  430. const auto result = Data::CloudTheme::Parse(session, data);
  431. session->data().cloudThemes().savedFromEditor(result);
  432. return result;
  433. });
  434. if (cloud.documentId && !state->themeContent.isEmpty()) {
  435. const auto document = session->data().document(cloud.documentId);
  436. document->setDataAndCache(state->themeContent);
  437. }
  438. KeepFromEditor(
  439. originalContent,
  440. originalParsed,
  441. cloud,
  442. state->themeContent,
  443. parsed,
  444. background);
  445. };
  446. const auto createTheme = [=](const MTPDocument &data) {
  447. const auto document = session->data().processDocument(data);
  448. state->requestId = api->request(MTPaccount_CreateTheme(
  449. MTP_flags(MTPaccount_CreateTheme::Flag::f_document),
  450. MTP_string(fields.slug),
  451. MTP_string(fields.title),
  452. document->mtpInput(),
  453. MTPVector<MTPInputThemeSettings>()
  454. )).done([=](const MTPTheme &result) {
  455. finish(result);
  456. }).fail([=](const MTP::Error &error) {
  457. fail(SaveErrorType::Other, error.type());
  458. }).send();
  459. };
  460. const auto updateTheme = [=](const MTPDocument &data) {
  461. using Flag = MTPaccount_UpdateTheme::Flag;
  462. const auto document = session->data().processDocument(data);
  463. const auto flags = Flag::f_title
  464. | Flag::f_slug
  465. | (data.type() == mtpc_documentEmpty
  466. ? Flag(0)
  467. : Flag::f_document);
  468. state->requestId = api->request(MTPaccount_UpdateTheme(
  469. MTP_flags(flags),
  470. MTP_string(Data::CloudThemes::Format()),
  471. MTP_inputTheme(MTP_long(fields.id), MTP_long(fields.accessHash)),
  472. MTP_string(fields.slug),
  473. MTP_string(fields.title),
  474. document->mtpInput(),
  475. MTPVector<MTPInputThemeSettings>()
  476. )).done([=](const MTPTheme &result) {
  477. finish(result);
  478. }).fail([=](const MTP::Error &error) {
  479. fail(SaveErrorType::Other, error.type());
  480. }).send();
  481. };
  482. const auto uploadTheme = [=](const UploadedMedia &data) {
  483. state->requestId = api->request(MTPaccount_UploadTheme(
  484. MTP_flags(MTPaccount_UploadTheme::Flag::f_thumb),
  485. data.info.file,
  486. *data.info.thumb,
  487. MTP_string(state->filename),
  488. MTP_string("application/x-tgtheme-tdesktop")
  489. )).done([=](const MTPDocument &result) {
  490. if (creating) {
  491. createTheme(result);
  492. } else {
  493. updateTheme(result);
  494. }
  495. }).fail([=](const MTP::Error &error) {
  496. fail(SaveErrorType::Other, error.type());
  497. }).send();
  498. };
  499. const auto uploadFile = [=](const QByteArray &theme) {
  500. const auto media = PrepareThemeMedia(
  501. session->mainDcId(),
  502. fields.title,
  503. theme);
  504. state->filename = media->filename;
  505. state->themeContent = theme;
  506. session->uploader().documentReady(
  507. ) | rpl::filter([=](const UploadedMedia &data) {
  508. return (data.fullId == state->id) && data.info.thumb.has_value();
  509. }) | rpl::start_with_next([=](const UploadedMedia &data) {
  510. uploadTheme(data);
  511. }, state->lifetime);
  512. session->uploader().upload(state->id, media);
  513. };
  514. const auto save = [=] {
  515. if (!creating && !changed) {
  516. updateTheme(MTP_documentEmpty(MTP_long(fields.documentId)));
  517. return;
  518. }
  519. state->generating = true;
  520. crl::async([=] {
  521. crl::on_main([=, ready = PackTheme(parsed)]{
  522. if (!state->generating) {
  523. return;
  524. }
  525. state->generating = false;
  526. uploadFile(ready);
  527. });
  528. });
  529. };
  530. const auto checkFields = [=] {
  531. state->requestId = api->request(MTPaccount_CreateTheme(
  532. MTP_flags(MTPaccount_CreateTheme::Flag::f_document),
  533. MTP_string(fields.slug),
  534. MTP_string(fields.title),
  535. MTP_inputDocumentEmpty(),
  536. MTPVector<MTPInputThemeSettings>()
  537. )).done([=](const MTPTheme &result) {
  538. save();
  539. }).fail([=](const MTP::Error &error) {
  540. if (error.type() == u"THEME_FILE_INVALID"_q) {
  541. save();
  542. } else {
  543. fail(SaveErrorType::Other, error.type());
  544. }
  545. }).send();
  546. };
  547. if (creating) {
  548. checkFields();
  549. } else {
  550. save();
  551. }
  552. return [=] {
  553. state->generating = false;
  554. api->request(base::take(state->requestId)).cancel();
  555. session->uploader().cancel(state->id);
  556. state->lifetime.destroy();
  557. };
  558. }
  559. } // namespace
  560. bool PaletteChanged(
  561. const QByteArray &editorPalette,
  562. const Data::CloudTheme &cloud) {
  563. auto object = Local::ReadThemeContent();
  564. const auto real = object.content.isEmpty()
  565. ? GenerateDefaultPalette()
  566. : ParseTheme(object, true).palette;
  567. return PaletteChanged(editorPalette, real, cloud);
  568. }
  569. void StartEditor(
  570. not_null<Window::Controller*> window,
  571. const Data::CloudTheme &cloud) {
  572. const auto path = EditingPalettePath();
  573. auto object = Local::ReadThemeContent();
  574. const auto palette = object.content.isEmpty()
  575. ? GenerateDefaultPalette()
  576. : ParseTheme(object, true).palette;
  577. if (palette.isEmpty() || !CopyColorsToPalette(path, palette, cloud)) {
  578. window->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));
  579. return;
  580. }
  581. if (Core::App().settings().systemDarkModeEnabled()) {
  582. Core::App().settings().setSystemDarkModeEnabled(false);
  583. Core::App().saveSettingsDelayed();
  584. }
  585. Background()->setEditingTheme(cloud);
  586. window->showRightColumn(Box<Editor>(window, cloud));
  587. }
  588. void CreateBox(
  589. not_null<Ui::GenericBox*> box,
  590. not_null<Window::Controller*> window) {
  591. CreateForExistingBox(box, window, Data::CloudTheme());
  592. }
  593. void CreateForExistingBox(
  594. not_null<Ui::GenericBox*> box,
  595. not_null<Window::Controller*> window,
  596. const Data::CloudTheme &cloud) {
  597. const auto amCreator = window->account().sessionExists()
  598. && (window->account().session().userId() == cloud.createdBy);
  599. box->setTitle(amCreator
  600. ? (rpl::single(cloud.title) | Ui::Text::ToWithEntities())
  601. : tr::lng_theme_editor_create_title(Ui::Text::WithEntities));
  602. box->addRow(object_ptr<Ui::FlatLabel>(
  603. box,
  604. (amCreator
  605. ? tr::lng_theme_editor_attach_description
  606. : tr::lng_theme_editor_create_description)(),
  607. st::boxDividerLabel));
  608. box->addRow(
  609. object_ptr<Ui::SettingsButton>(
  610. box,
  611. tr::lng_theme_editor_import_existing(),
  612. st::createThemeImportButton),
  613. style::margins(
  614. 0,
  615. st::boxRowPadding.left(),
  616. 0,
  617. 0)
  618. )->addClickHandler([=] {
  619. ImportFromFile(&window->account().session(), box);
  620. });
  621. const auto done = [=] {
  622. box->closeBox();
  623. StartEditor(window, cloud);
  624. };
  625. base::install_event_filter(box, box, [=](not_null<QEvent*> event) {
  626. if (event->type() == QEvent::KeyPress) {
  627. const auto key = static_cast<QKeyEvent*>(event.get())->key();
  628. if (key == Qt::Key_Enter || key == Qt::Key_Return) {
  629. done();
  630. return base::EventFilterResult::Cancel;
  631. }
  632. }
  633. return base::EventFilterResult::Continue;
  634. });
  635. box->addButton(tr::lng_theme_editor_create(), done);
  636. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  637. }
  638. void SaveTheme(
  639. not_null<Window::Controller*> window,
  640. const Data::CloudTheme &cloud,
  641. const QByteArray &palette,
  642. Fn<void()> unlock) {
  643. Expects(window->account().sessionExists());
  644. using Data::CloudTheme;
  645. const auto save = [=](const CloudTheme &fields) {
  646. unlock();
  647. window->show(Box(SaveThemeBox, window, fields, palette));
  648. };
  649. if (cloud.id) {
  650. window->account().session().api().request(MTPaccount_GetTheme(
  651. MTP_string(Data::CloudThemes::Format()),
  652. MTP_inputTheme(MTP_long(cloud.id), MTP_long(cloud.accessHash))
  653. )).done([=](const MTPTheme &result) {
  654. result.match([&](const MTPDtheme &data) {
  655. save(CloudTheme::Parse(&window->account().session(), data));
  656. });
  657. }).fail([=] {
  658. save(CloudTheme());
  659. }).send();
  660. } else {
  661. save(CloudTheme());
  662. }
  663. }
  664. struct CollectedData {
  665. QByteArray originalContent;
  666. ParsedTheme originalParsed;
  667. ParsedTheme parsed;
  668. QImage background;
  669. QColor accent;
  670. };
  671. [[nodiscard]] CollectedData CollectData(const QByteArray &palette) {
  672. const auto original = Local::ReadThemeContent();
  673. const auto originalContent = original.content;
  674. // We don't need default palette here, because in case of it we are
  675. // not interested if the palette was changed, we'll save it anyway.
  676. const auto originalParsed = originalContent.isEmpty()
  677. ? ParsedTheme() // GenerateDefaultPalette()
  678. : ParseTheme(original);
  679. const auto background = Background()->createCurrentImage();
  680. const auto changed = !Data::IsThemeWallPaper(Background()->paper())
  681. || originalParsed.background.isEmpty()
  682. || ColorizerForTheme(original.pathAbsolute);
  683. auto parsed = ParsedTheme();
  684. parsed.palette = StripCloudTextFields(palette);
  685. parsed.isPng = false;
  686. if (changed) {
  687. QBuffer buffer(&parsed.background);
  688. background.save(&buffer, "JPG", 87);
  689. } else {
  690. // Use existing background serialization.
  691. parsed.background = originalParsed.background;
  692. parsed.isPng = originalParsed.isPng;
  693. }
  694. const auto accent = st::windowActiveTextFg->c;
  695. return { originalContent, originalParsed, parsed, background, accent };
  696. }
  697. QByteArray CollectForExport(const QByteArray &palette) {
  698. return PackTheme(CollectData(palette).parsed);
  699. }
  700. void SaveThemeBox(
  701. not_null<Ui::GenericBox*> box,
  702. not_null<Window::Controller*> window,
  703. const Data::CloudTheme &cloud,
  704. const QByteArray &palette) {
  705. Expects(window->account().sessionExists());
  706. const auto collected = CollectData(palette);
  707. const auto title = cloud.title.isEmpty()
  708. ? GenerateName(collected.accent)
  709. : cloud.title;
  710. box->setTitle(tr::lng_theme_editor_save_title(Ui::Text::WithEntities));
  711. const auto name = box->addRow(object_ptr<Ui::InputField>(
  712. box,
  713. st::defaultInputField,
  714. tr::lng_theme_editor_name(),
  715. title));
  716. const auto linkWrap = box->addRow(
  717. object_ptr<Ui::RpWidget>(box),
  718. style::margins(
  719. st::boxRowPadding.left(),
  720. st::themesSmallSkip,
  721. st::boxRowPadding.right(),
  722. st::boxRowPadding.bottom()));
  723. const auto link = Ui::CreateChild<Ui::UsernameInput>(
  724. linkWrap,
  725. st::createThemeLink,
  726. rpl::single(u"link"_q),
  727. cloud.slug.isEmpty() ? GenerateSlug() : cloud.slug,
  728. window->account().session().createInternalLink(QString()));
  729. linkWrap->widthValue(
  730. ) | rpl::start_with_next([=](int width) {
  731. link->resize(width, link->height());
  732. link->moveToLeft(0, 0, width);
  733. }, link->lifetime());
  734. link->heightValue(
  735. ) | rpl::start_with_next([=](int height) {
  736. linkWrap->resize(linkWrap->width(), height);
  737. }, link->lifetime());
  738. link->setLinkPlaceholder(
  739. window->account().session().createInternalLink(u"addtheme/"_q));
  740. link->setPlaceholderHidden(false);
  741. link->setMaxLength(kMaxSlugSize);
  742. box->addRow(
  743. object_ptr<Ui::FlatLabel>(
  744. box,
  745. tr::lng_theme_editor_link_about(),
  746. st::boxDividerLabel),
  747. style::margins(
  748. st::boxRowPadding.left(),
  749. st::themesSmallSkip,
  750. st::boxRowPadding.right(),
  751. st::boxRowPadding.bottom()));
  752. box->addRow(
  753. object_ptr<Ui::FlatLabel>(
  754. box,
  755. tr::lng_theme_editor_background_image(),
  756. st::defaultSubsectionTitle),
  757. st::defaultSubsectionTitlePadding);
  758. const auto back = box->addRow(
  759. object_ptr<BackgroundSelector>(
  760. box,
  761. collected.background,
  762. collected.parsed),
  763. style::margins(
  764. st::boxRowPadding.left(),
  765. st::themesSmallSkip,
  766. st::boxRowPadding.right(),
  767. st::boxRowPadding.bottom()));
  768. box->setFocusCallback([=] { name->setFocusFast(); });
  769. box->setWidth(st::boxWideWidth);
  770. const auto saving = box->lifetime().make_state<bool>();
  771. const auto cancel = std::make_shared<Fn<void()>>(nullptr);
  772. box->lifetime().add([=] { if (*cancel) (*cancel)(); });
  773. const auto save = [=] {
  774. if (*saving) {
  775. return;
  776. }
  777. const auto done = crl::guard(box, [=] {
  778. box->closeBox();
  779. window->showRightColumn(nullptr);
  780. });
  781. const auto fail = crl::guard(box, [=](
  782. SaveErrorType type,
  783. const QString &error) {
  784. *saving = false;
  785. box->showLoading(false);
  786. if (error == u"THEME_TITLE_INVALID"_q) {
  787. type = SaveErrorType::Name;
  788. } else if (error == u"THEME_SLUG_INVALID"_q) {
  789. type = SaveErrorType::Link;
  790. } else if (error == u"THEME_SLUG_OCCUPIED"_q) {
  791. box->showToast(
  792. tr::lng_create_channel_link_occupied(tr::now));
  793. type = SaveErrorType::Link;
  794. } else if (!error.isEmpty()) {
  795. box->showToast(error);
  796. }
  797. if (type == SaveErrorType::Name) {
  798. name->showError();
  799. } else if (type == SaveErrorType::Link) {
  800. link->showError();
  801. }
  802. });
  803. auto fields = cloud;
  804. fields.title = name->getLastText().trimmed();
  805. fields.slug = link->getLastText().trimmed();
  806. if (fields.title.isEmpty()) {
  807. fail(SaveErrorType::Name, QString());
  808. return;
  809. } else if (!IsGoodSlug(fields.slug)) {
  810. fail(SaveErrorType::Link, QString());
  811. return;
  812. }
  813. *saving = true;
  814. box->showLoading(true);
  815. *cancel = SavePreparedTheme(
  816. window,
  817. back->result(),
  818. back->image(),
  819. collected.originalContent,
  820. collected.originalParsed,
  821. fields,
  822. done,
  823. fail);
  824. };
  825. box->addButton(tr::lng_settings_save(), save);
  826. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  827. }
  828. ParsedTheme ParseTheme(
  829. const Object &theme,
  830. bool onlyPalette,
  831. bool parseCurrent) {
  832. auto raw = ParsedTheme();
  833. raw.palette = theme.content;
  834. const auto result = [&] {
  835. if (const auto colorizer = ColorizerForTheme(theme.pathAbsolute)) {
  836. raw.palette = Editor::ColorizeInContent(
  837. std::move(raw.palette),
  838. colorizer);
  839. }
  840. if (parseCurrent) {
  841. raw.palette = ReplaceAdjustableColors(std::move(raw.palette));
  842. }
  843. return raw;
  844. };
  845. zlib::FileToRead file(theme.content);
  846. unz_global_info globalInfo = { 0 };
  847. file.getGlobalInfo(&globalInfo);
  848. if (file.error() != UNZ_OK) {
  849. return result();
  850. }
  851. raw.palette = file.readFileContent("colors.tdesktop-theme", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);
  852. if (file.error() == UNZ_END_OF_LIST_OF_FILE) {
  853. file.clearError();
  854. raw.palette = file.readFileContent("colors.tdesktop-palette", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);
  855. }
  856. if (file.error() != UNZ_OK) {
  857. LOG(("Theme Error: could not read 'colors.tdesktop-theme' or 'colors.tdesktop-palette' in the theme file."));
  858. return ParsedTheme();
  859. } else if (onlyPalette) {
  860. return result();
  861. }
  862. const auto fromFile = [&](const char *filename) {
  863. raw.background = file.readFileContent(filename, zlib::kCaseInsensitive, kThemeBackgroundSizeLimit);
  864. if (file.error() == UNZ_OK) {
  865. return true;
  866. } else if (file.error() == UNZ_END_OF_LIST_OF_FILE) {
  867. file.clearError();
  868. return true;
  869. }
  870. LOG(("Theme Error: could not read '%1' in the theme file.").arg(filename));
  871. return false;
  872. };
  873. if (!fromFile("background.jpg") || !raw.background.isEmpty()) {
  874. return raw.background.isEmpty() ? ParsedTheme() : result();
  875. }
  876. raw.isPng = true;
  877. if (!fromFile("background.png") || !raw.background.isEmpty()) {
  878. return raw.background.isEmpty() ? ParsedTheme() : result();
  879. }
  880. raw.tiled = true;
  881. if (!fromFile("tiled.png") || !raw.background.isEmpty()) {
  882. return raw.background.isEmpty() ? ParsedTheme() : result();
  883. }
  884. raw.isPng = false;
  885. if (!fromFile("background.jpg") || !raw.background.isEmpty()) {
  886. return raw.background.isEmpty() ? ParsedTheme() : result();
  887. }
  888. return result();
  889. }
  890. [[nodiscard]] QString GenerateSlug() {
  891. const auto letters = uint8('Z' + 1 - 'A');
  892. const auto digits = uint8('9' + 1 - '0');
  893. const auto firstValues = uint8(2 * letters);
  894. const auto values = uint8(2 * letters + digits);
  895. auto result = QString();
  896. result.reserve(kRandomSlugSize);
  897. for (auto i = 0; i != kRandomSlugSize; ++i) {
  898. const auto value = i
  899. ? (base::RandomValue<uint8>() % values)
  900. : (base::RandomValue<uint8>() % firstValues);
  901. if (value < letters) {
  902. result.append(char('A' + value));
  903. } else if (value < 2 * letters) {
  904. result.append(char('a' + (value - letters)));
  905. } else {
  906. result.append(char('0' + (value - 2 * letters)));
  907. }
  908. }
  909. return result;
  910. }
  911. } // namespace Theme
  912. } // namespace Window