passport_panel_edit_scans.cpp 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982
  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 "passport/passport_panel_edit_scans.h"
  8. #include "passport/passport_panel_controller.h"
  9. #include "passport/ui/passport_details_row.h"
  10. #include "ui/widgets/buttons.h"
  11. #include "ui/widgets/labels.h"
  12. #include "ui/widgets/box_content_divider.h"
  13. #include "ui/wrap/fade_wrap.h"
  14. #include "ui/wrap/slide_wrap.h"
  15. #include "ui/wrap/vertical_layout.h"
  16. #include "ui/chat/attach/attach_prepare.h"
  17. #include "ui/text/text_utilities.h" // Ui::Text::ToUpper
  18. #include "ui/text/text_options.h"
  19. #include "ui/image/image_prepare.h"
  20. #include "ui/painter.h"
  21. #include "core/file_utilities.h"
  22. #include "lang/lang_keys.h"
  23. #include "boxes/abstract_box.h"
  24. #include "storage/storage_media_prepare.h"
  25. #include "storage/file_upload.h" // For Storage::kUseBigFilesFrom.
  26. #include "styles/style_layers.h"
  27. #include "styles/style_passport.h"
  28. #include <QtCore/QBuffer>
  29. namespace Passport {
  30. namespace {
  31. constexpr auto kMaxDimensions = 2048;
  32. constexpr auto kMaxSize = 10 * 1024 * 1024;
  33. constexpr auto kJpegQuality = 89;
  34. static_assert(kMaxSize <= Storage::kUseBigFilesFrom);
  35. std::variant<ReadScanError, QByteArray> ProcessImage(QByteArray &&bytes) {
  36. auto read = Images::Read({
  37. .content = base::take(bytes),
  38. .forceOpaque = true,
  39. });
  40. auto &image = read.image;
  41. if (image.isNull()) {
  42. return ReadScanError::CantReadImage;
  43. } else if (!Ui::ValidateThumbDimensions(image.width(), image.height())) {
  44. return ReadScanError::BadImageSize;
  45. }
  46. if (std::max(image.width(), image.height()) > kMaxDimensions) {
  47. image = std::move(image).scaled(
  48. kMaxDimensions,
  49. kMaxDimensions,
  50. Qt::KeepAspectRatio,
  51. Qt::SmoothTransformation);
  52. }
  53. auto result = QByteArray();
  54. {
  55. QBuffer buffer(&result);
  56. if (!image.save(&buffer, "JPG", kJpegQuality)) {
  57. return ReadScanError::Unknown;
  58. }
  59. base::take(image);
  60. }
  61. if (result.isEmpty()) {
  62. return ReadScanError::Unknown;
  63. } else if (result.size() > kMaxSize) {
  64. return ReadScanError::FileTooLarge;
  65. }
  66. return result;
  67. }
  68. } // namespace
  69. class ScanButton : public Ui::AbstractButton {
  70. public:
  71. ScanButton(
  72. QWidget *parent,
  73. const style::PassportScanRow &st,
  74. const QString &name,
  75. const QString &status,
  76. bool deleted,
  77. bool error);
  78. void setImage(const QImage &image);
  79. void setStatus(const QString &status);
  80. void setDeleted(bool deleted);
  81. void setError(bool error);
  82. rpl::producer<> deleteClicks() const {
  83. return _delete->entity()->clicks() | rpl::to_empty;
  84. }
  85. rpl::producer<> restoreClicks() const {
  86. return _restore->entity()->clicks() | rpl::to_empty;
  87. }
  88. protected:
  89. int resizeGetHeight(int newWidth) override;
  90. void paintEvent(QPaintEvent *e) override;
  91. private:
  92. int countAvailableWidth() const;
  93. const style::PassportScanRow &_st;
  94. Ui::Text::String _name;
  95. Ui::Text::String _status;
  96. int _nameHeight = 0;
  97. int _statusHeight = 0;
  98. bool _error = false;
  99. QImage _image;
  100. object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _delete;
  101. object_ptr<Ui::FadeWrapScaled<Ui::RoundButton>> _restore;
  102. };
  103. struct EditScans::SpecialScan {
  104. SpecialScan(ScanInfo &&file);
  105. ScanInfo file;
  106. QPointer<Ui::SlideWrap<Ui::FlatLabel>> header;
  107. QPointer<Ui::VerticalLayout> wrap;
  108. base::unique_qptr<Ui::SlideWrap<ScanButton>> row;
  109. QPointer<Ui::SettingsButton> upload;
  110. bool errorShown = false;
  111. Ui::Animations::Simple errorAnimation;
  112. rpl::variable<bool> rowCreated;
  113. };
  114. void UpdateFileRow(
  115. not_null<ScanButton*> button,
  116. const ScanInfo &info) {
  117. button->setStatus(info.status);
  118. button->setImage(info.thumb);
  119. button->setDeleted(info.deleted);
  120. button->setError(!info.error.isEmpty());
  121. }
  122. base::unique_qptr<Ui::SlideWrap<ScanButton>> CreateScan(
  123. not_null<Ui::VerticalLayout*> parent,
  124. const ScanInfo &info,
  125. const QString &name) {
  126. auto result = base::unique_qptr<Ui::SlideWrap<ScanButton>>(
  127. parent->add(object_ptr<Ui::SlideWrap<ScanButton>>(
  128. parent,
  129. object_ptr<ScanButton>(
  130. parent,
  131. st::passportScanRow,
  132. name,
  133. info.status,
  134. info.deleted,
  135. !info.error.isEmpty()))));
  136. result->entity()->setImage(info.thumb);
  137. return result;
  138. }
  139. EditScans::List::List(
  140. not_null<PanelController*> controller,
  141. ScanListData &&data)
  142. : controller(controller)
  143. , files(std::move(data.files))
  144. , initialCount(int(files.size()))
  145. , errorMissing(data.errorMissing) {
  146. }
  147. EditScans::List::List(
  148. not_null<PanelController*> controller)
  149. : List(controller, std::nullopt)
  150. {
  151. }
  152. EditScans::List::List(
  153. not_null<PanelController*> controller,
  154. std::optional<ScanListData> &&data)
  155. : controller(controller)
  156. , files(data ? std::move(data->files) : std::vector<ScanInfo>())
  157. , initialCount(data ? base::make_optional(int(files.size())) : std::nullopt)
  158. , errorMissing(data ? std::move(data->errorMissing) : QString()) {
  159. }
  160. bool EditScans::List::uploadedSomeMore() const {
  161. if (!initialCount) {
  162. return false;
  163. }
  164. const auto from = begin(files) + *initialCount;
  165. const auto till = end(files);
  166. return std::find_if(from, till, [](const ScanInfo &file) {
  167. return !file.deleted;
  168. }) != till;
  169. }
  170. bool EditScans::List::uploadMoreRequired() const {
  171. if (!upload) {
  172. return false;
  173. }
  174. const auto exists = ranges::any_of(
  175. files,
  176. [](const ScanInfo &file) { return !file.deleted; });
  177. if (!exists) {
  178. return true;
  179. }
  180. const auto errorExists = ranges::any_of(
  181. files,
  182. [](const ScanInfo &file) { return !file.error.isEmpty(); });
  183. return (errorExists || uploadMoreError) && !uploadedSomeMore();
  184. }
  185. Ui::SlideWrap<ScanButton> *EditScans::List::nonDeletedErrorRow() const {
  186. const auto nonDeletedErrorIt = ranges::find_if(
  187. files,
  188. [](const ScanInfo &file) {
  189. return !file.error.isEmpty() && !file.deleted;
  190. });
  191. if (nonDeletedErrorIt == end(files)) {
  192. return nullptr;
  193. }
  194. const auto index = (nonDeletedErrorIt - begin(files));
  195. return rows[index].get();
  196. }
  197. rpl::producer<QString> EditScans::List::uploadButtonText() const {
  198. return (files.empty()
  199. ? tr::lng_passport_upload_scans
  200. : tr::lng_passport_upload_more)() | Ui::Text::ToUpper();
  201. }
  202. void EditScans::List::hideError() {
  203. toggleError(false);
  204. }
  205. void EditScans::List::toggleError(bool shown) {
  206. if (errorShown != shown) {
  207. errorShown = shown;
  208. errorAnimation.start(
  209. [=] { errorAnimationCallback(); },
  210. errorShown ? 0. : 1.,
  211. errorShown ? 1. : 0.,
  212. st::passportDetailsField.duration);
  213. }
  214. }
  215. void EditScans::List::errorAnimationCallback() {
  216. const auto error = errorAnimation.value(errorShown ? 1. : 0.);
  217. if (error == 0.) {
  218. upload->setColorOverride(std::nullopt);
  219. } else {
  220. upload->setColorOverride(anim::color(
  221. st::passportUploadButton.textFg,
  222. st::boxTextFgError,
  223. error));
  224. }
  225. }
  226. void EditScans::List::updateScan(ScanInfo &&info, int width) {
  227. const auto i = ranges::find(files, info.key, [](const ScanInfo &file) {
  228. return file.key;
  229. });
  230. if (i != files.end()) {
  231. *i = std::move(info);
  232. const auto scan = rows[i - files.begin()]->entity();
  233. UpdateFileRow(scan, *i);
  234. if (!i->deleted) {
  235. hideError();
  236. }
  237. } else {
  238. files.push_back(std::move(info));
  239. pushScan(files.back());
  240. wrap->resizeToWidth(width);
  241. rows.back()->show(anim::type::normal);
  242. if (divider) {
  243. divider->hide(anim::type::normal);
  244. }
  245. header->show(anim::type::normal);
  246. uploadTexts.fire(uploadButtonText());
  247. }
  248. }
  249. void EditScans::List::pushScan(const ScanInfo &info) {
  250. const auto index = rows.size();
  251. const auto type = info.type;
  252. rows.push_back(CreateScan(
  253. wrap,
  254. info,
  255. tr::lng_passport_scan_index(tr::now, lt_index, QString::number(index + 1))));
  256. rows.back()->hide(anim::type::instant);
  257. const auto scan = rows.back()->entity();
  258. scan->deleteClicks(
  259. ) | rpl::start_with_next([=] {
  260. controller->deleteScan(type, index);
  261. }, scan->lifetime());
  262. scan->restoreClicks(
  263. ) | rpl::start_with_next([=] {
  264. controller->restoreScan(type, index);
  265. }, scan->lifetime());
  266. hideError();
  267. }
  268. ScanButton::ScanButton(
  269. QWidget *parent,
  270. const style::PassportScanRow &st,
  271. const QString &name,
  272. const QString &status,
  273. bool deleted,
  274. bool error)
  275. : AbstractButton(parent)
  276. , _st(st)
  277. , _name(
  278. st::passportScanNameStyle,
  279. name,
  280. Ui::NameTextOptions())
  281. , _status(
  282. st::defaultTextStyle,
  283. status,
  284. Ui::NameTextOptions())
  285. , _error(error)
  286. , _delete(this, object_ptr<Ui::IconButton>(this, _st.remove))
  287. , _restore(
  288. this,
  289. object_ptr<Ui::RoundButton>(
  290. this,
  291. tr::lng_passport_delete_scan_undo(),
  292. _st.restore)) {
  293. _delete->toggle(!deleted, anim::type::instant);
  294. _restore->toggle(deleted, anim::type::instant);
  295. }
  296. void ScanButton::setImage(const QImage &image) {
  297. _image = image;
  298. update();
  299. }
  300. void ScanButton::setStatus(const QString &status) {
  301. _status.setText(
  302. st::defaultTextStyle,
  303. status,
  304. Ui::NameTextOptions());
  305. update();
  306. }
  307. void ScanButton::setDeleted(bool deleted) {
  308. _delete->toggle(!deleted, anim::type::instant);
  309. _restore->toggle(deleted, anim::type::instant);
  310. update();
  311. }
  312. void ScanButton::setError(bool error) {
  313. _error = error;
  314. update();
  315. }
  316. int ScanButton::resizeGetHeight(int newWidth) {
  317. _nameHeight = st::semiboldFont->height;
  318. _statusHeight = st::normalFont->height;
  319. const auto result = _st.padding.top() + _st.size + _st.padding.bottom();
  320. const auto right = _st.padding.right();
  321. _delete->moveToRight(
  322. right,
  323. (result - _delete->height()) / 2,
  324. newWidth);
  325. _restore->moveToRight(
  326. right,
  327. (result - _restore->height()) / 2,
  328. newWidth);
  329. return result + st::lineWidth;
  330. }
  331. int ScanButton::countAvailableWidth() const {
  332. return width()
  333. - _st.padding.left()
  334. - _st.textLeft
  335. - _st.padding.right()
  336. - std::max(_delete->width(), _restore->width());
  337. }
  338. void ScanButton::paintEvent(QPaintEvent *e) {
  339. Painter p(this);
  340. const auto left = _st.padding.left();
  341. const auto top = _st.padding.top();
  342. p.fillRect(
  343. left,
  344. height() - _st.border,
  345. width() - left,
  346. _st.border,
  347. _st.borderFg);
  348. const auto deleted = _restore->toggled();
  349. if (deleted) {
  350. p.setOpacity(st::passportScanDeletedOpacity);
  351. }
  352. if (_image.isNull()) {
  353. p.fillRect(left, top, _st.size, _st.size, Qt::black);
  354. } else {
  355. PainterHighQualityEnabler hq(p);
  356. const auto fromRect = [&] {
  357. if (_image.width() > _image.height()) {
  358. const auto shift = (_image.width() - _image.height()) / 2;
  359. return QRect(shift, 0, _image.height(), _image.height());
  360. } else {
  361. const auto shift = (_image.height() - _image.width()) / 2;
  362. return QRect(0, shift, _image.width(), _image.width());
  363. }
  364. }();
  365. p.drawImage(QRect(left, top, _st.size, _st.size), _image, fromRect);
  366. }
  367. const auto availableWidth = countAvailableWidth();
  368. p.setPen(st::windowFg);
  369. _name.drawLeftElided(
  370. p,
  371. left + _st.textLeft,
  372. top + _st.nameTop,
  373. availableWidth,
  374. width());
  375. p.setPen((_error && !deleted)
  376. ? st::boxTextFgError
  377. : st::windowSubTextFg);
  378. _status.drawLeftElided(
  379. p,
  380. left + _st.textLeft,
  381. top + _st.statusTop,
  382. availableWidth,
  383. width());
  384. }
  385. EditScans::SpecialScan::SpecialScan(ScanInfo &&file)
  386. : file(std::move(file)) {
  387. }
  388. EditScans::EditScans(
  389. QWidget *parent,
  390. not_null<PanelController*> controller,
  391. const QString &header,
  392. const QString &error,
  393. ScanListData &&scans,
  394. std::optional<ScanListData> &&translations)
  395. : RpWidget(parent)
  396. , _controller(controller)
  397. , _error(error)
  398. , _content(this)
  399. , _scansList(_controller, std::move(scans))
  400. , _translationsList(_controller, std::move(translations)) {
  401. setupScans(header);
  402. }
  403. EditScans::EditScans(
  404. QWidget *parent,
  405. not_null<PanelController*> controller,
  406. const QString &header,
  407. const QString &error,
  408. std::map<FileType, ScanInfo> &&specialFiles,
  409. std::optional<ScanListData> &&translations)
  410. : RpWidget(parent)
  411. , _controller(controller)
  412. , _error(error)
  413. , _content(this)
  414. , _scansList(_controller)
  415. , _translationsList(_controller, std::move(translations)) {
  416. setupSpecialScans(header, std::move(specialFiles));
  417. }
  418. std::optional<int> EditScans::validateGetErrorTop() {
  419. auto result = std::optional<int>();
  420. const auto suggestResult = [&](int value) {
  421. if (!result || *result > value) {
  422. result = value;
  423. }
  424. };
  425. if (_commonError && !somethingChanged()) {
  426. suggestResult(_commonError->y());
  427. }
  428. const auto suggestList = [&](FileType type) {
  429. auto &list = this->list(type);
  430. if (list.uploadMoreRequired()) {
  431. list.toggleError(true);
  432. suggestResult((list.files.size() > 5)
  433. ? list.upload->y()
  434. : list.header->y());
  435. }
  436. if (const auto row = list.nonDeletedErrorRow()) {
  437. //toggleError(true);
  438. suggestResult(row->y());
  439. }
  440. };
  441. suggestList(FileType::Scan);
  442. for (const auto &[type, scan] : _specialScans) {
  443. if (!scan.file.key.id
  444. || scan.file.deleted
  445. || !scan.file.error.isEmpty()) {
  446. toggleSpecialScanError(type, true);
  447. suggestResult(scan.header ? scan.header->y() : scan.wrap->y());
  448. }
  449. }
  450. suggestList(FileType::Translation);
  451. return result;
  452. }
  453. EditScans::List &EditScans::list(FileType type) {
  454. switch (type) {
  455. case FileType::Scan: return _scansList;
  456. case FileType::Translation: return _translationsList;
  457. }
  458. Unexpected("Type in EditScans::list().");
  459. }
  460. const EditScans::List &EditScans::list(FileType type) const {
  461. switch (type) {
  462. case FileType::Scan: return _scansList;
  463. case FileType::Translation: return _translationsList;
  464. }
  465. Unexpected("Type in EditScans::list() const.");
  466. }
  467. void EditScans::setupScans(const QString &header) {
  468. const auto inner = _content.data();
  469. inner->move(0, 0);
  470. if (!_error.isEmpty()) {
  471. _commonError = inner->add(
  472. object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
  473. inner,
  474. object_ptr<Ui::FlatLabel>(
  475. inner,
  476. _error,
  477. st::passportVerifyErrorLabel),
  478. st::passportValueErrorPadding));
  479. _commonError->toggle(true, anim::type::instant);
  480. }
  481. setupList(inner, FileType::Scan, header);
  482. setupList(inner, FileType::Translation, tr::lng_passport_translation(tr::now));
  483. init();
  484. }
  485. void EditScans::setupList(
  486. not_null<Ui::VerticalLayout*> container,
  487. FileType type,
  488. const QString &header) {
  489. auto &list = this->list(type);
  490. if (!list.initialCount) {
  491. return;
  492. }
  493. if (type == FileType::Scan) {
  494. list.divider = container->add(
  495. object_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(
  496. container,
  497. object_ptr<Ui::BoxContentDivider>(
  498. container,
  499. st::passportFormDividerHeight)));
  500. list.divider->toggle(list.files.empty(), anim::type::instant);
  501. }
  502. list.header = container->add(
  503. object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
  504. container,
  505. object_ptr<Ui::FlatLabel>(
  506. container,
  507. header,
  508. st::passportFormHeader),
  509. st::passportUploadHeaderPadding));
  510. list.header->toggle(
  511. !list.divider || !list.files.empty(),
  512. anim::type::instant);
  513. if (!list.errorMissing.isEmpty()) {
  514. list.uploadMoreError = container->add(
  515. object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
  516. container,
  517. object_ptr<Ui::FlatLabel>(
  518. container,
  519. list.errorMissing,
  520. st::passportVerifyErrorLabel),
  521. st::passportUploadErrorPadding));
  522. list.uploadMoreError->toggle(true, anim::type::instant);
  523. }
  524. list.wrap = container->add(object_ptr<Ui::VerticalLayout>(container));
  525. for (const auto &scan : list.files) {
  526. list.pushScan(scan);
  527. list.rows.back()->show(anim::type::instant);
  528. }
  529. list.upload = container->add(
  530. object_ptr<Ui::SettingsButton>(
  531. container,
  532. list.uploadTexts.events_starting_with(
  533. list.uploadButtonText()
  534. ) | rpl::flatten_latest(),
  535. st::passportUploadButton),
  536. st::passportUploadButtonPadding);
  537. list.upload->addClickHandler([=] {
  538. chooseScan(type);
  539. });
  540. container->add(object_ptr<Ui::BoxContentDivider>(
  541. container,
  542. st::passportFormDividerHeight));
  543. }
  544. void EditScans::setupSpecialScans(
  545. const QString &header,
  546. std::map<FileType, ScanInfo> &&files) {
  547. const auto requiresBothSides = files.find(FileType::ReverseSide)
  548. != end(files);
  549. const auto uploadText = [=](FileType type, bool hasScan) {
  550. switch (type) {
  551. case FileType::FrontSide:
  552. return requiresBothSides
  553. ? (hasScan
  554. ? tr::lng_passport_reupload_front_side
  555. : tr::lng_passport_upload_front_side)
  556. : (hasScan
  557. ? tr::lng_passport_reupload_main_page
  558. : tr::lng_passport_upload_main_page);
  559. case FileType::ReverseSide:
  560. return hasScan
  561. ? tr::lng_passport_reupload_reverse_side
  562. : tr::lng_passport_upload_reverse_side;
  563. case FileType::Selfie:
  564. return hasScan
  565. ? tr::lng_passport_reupload_selfie
  566. : tr::lng_passport_upload_selfie;
  567. }
  568. Unexpected("Type in special row upload key.");
  569. };
  570. const auto description = [&](FileType type) {
  571. switch (type) {
  572. case FileType::FrontSide:
  573. return requiresBothSides
  574. ? tr::lng_passport_front_side_description
  575. : tr::lng_passport_main_page_description;
  576. case FileType::ReverseSide:
  577. return tr::lng_passport_reverse_side_description;
  578. case FileType::Selfie:
  579. return tr::lng_passport_selfie_description;
  580. }
  581. Unexpected("Type in special row upload key.");
  582. };
  583. const auto inner = _content.data();
  584. inner->move(0, 0);
  585. if (!_error.isEmpty()) {
  586. _commonError = inner->add(
  587. object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
  588. inner,
  589. object_ptr<Ui::FlatLabel>(
  590. inner,
  591. _error,
  592. st::passportVerifyErrorLabel),
  593. st::passportValueErrorPadding));
  594. _commonError->toggle(true, anim::type::instant);
  595. }
  596. for (auto &[type, info] : files) {
  597. const auto i = _specialScans.emplace(
  598. type,
  599. SpecialScan(std::move(info))).first;
  600. auto &scan = i->second;
  601. if (_specialScans.size() == 1) {
  602. scan.header = inner->add(
  603. object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
  604. inner,
  605. object_ptr<Ui::FlatLabel>(
  606. inner,
  607. header,
  608. st::passportFormHeader),
  609. st::passportUploadHeaderPadding));
  610. scan.header->toggle(scan.file.key.id != 0, anim::type::instant);
  611. }
  612. scan.wrap = inner->add(object_ptr<Ui::VerticalLayout>(inner));
  613. if (scan.file.key.id) {
  614. createSpecialScanRow(scan, scan.file, requiresBothSides);
  615. }
  616. auto label = scan.rowCreated.value(
  617. ) | rpl::map([=, type = type](bool created) {
  618. return uploadText(type, created)();
  619. }) | rpl::flatten_latest(
  620. ) | Ui::Text::ToUpper();
  621. scan.upload = inner->add(
  622. object_ptr<Ui::SettingsButton>(
  623. inner,
  624. std::move(label),
  625. st::passportUploadButton),
  626. st::passportUploadButtonPadding);
  627. scan.upload->addClickHandler([=, type = type] {
  628. chooseScan(type);
  629. });
  630. inner->add(object_ptr<Ui::DividerLabel>(
  631. inner,
  632. object_ptr<Ui::FlatLabel>(
  633. inner,
  634. description(type)(tr::now),
  635. st::boxDividerLabel),
  636. st::passportFormLabelPadding));
  637. }
  638. setupList(inner, FileType::Translation, tr::lng_passport_translation(tr::now));
  639. init();
  640. }
  641. void EditScans::init() {
  642. _controller->scanUpdated(
  643. ) | rpl::start_with_next([=](ScanInfo &&info) {
  644. updateScan(std::move(info));
  645. }, lifetime());
  646. widthValue(
  647. ) | rpl::start_with_next([=](int width) {
  648. _content->resizeToWidth(width);
  649. }, _content->lifetime());
  650. _content->heightValue(
  651. ) | rpl::start_with_next([=](int height) {
  652. resize(width(), height);
  653. }, _content->lifetime());
  654. }
  655. void EditScans::updateScan(ScanInfo &&info) {
  656. if (info.type != FileType::Scan && info.type != FileType::Translation) {
  657. updateSpecialScan(std::move(info));
  658. return;
  659. }
  660. list(info.type).updateScan(std::move(info), width());
  661. updateErrorLabels();
  662. }
  663. void EditScans::scanFieldsChanged(bool changed) {
  664. if (_scanFieldsChanged != changed) {
  665. _scanFieldsChanged = changed;
  666. updateErrorLabels();
  667. }
  668. }
  669. void EditScans::updateErrorLabels() {
  670. const auto updateList = [&](FileType type) {
  671. auto &list = this->list(type);
  672. if (list.uploadMoreError) {
  673. list.uploadMoreError->toggle(
  674. !list.uploadedSomeMore(),
  675. anim::type::normal);
  676. }
  677. };
  678. updateList(FileType::Scan);
  679. updateList(FileType::Translation);
  680. if (_commonError) {
  681. _commonError->toggle(!somethingChanged(), anim::type::normal);
  682. }
  683. }
  684. bool EditScans::somethingChanged() const {
  685. return list(FileType::Scan).uploadedSomeMore()
  686. || list(FileType::Translation).uploadedSomeMore()
  687. || _scanFieldsChanged
  688. || _specialScanChanged;
  689. }
  690. void EditScans::updateSpecialScan(ScanInfo &&info) {
  691. Expects(info.key.id != 0);
  692. const auto type = info.type;
  693. const auto i = _specialScans.find(type);
  694. if (i == end(_specialScans)) {
  695. return;
  696. }
  697. auto &scan = i->second;
  698. if (scan.file.key.id) {
  699. UpdateFileRow(scan.row->entity(), info);
  700. scan.rowCreated = !info.deleted;
  701. if (scan.file.key.id != info.key.id) {
  702. specialScanChanged(type, true);
  703. }
  704. } else {
  705. const auto requiresBothSides
  706. = (_specialScans.find(FileType::ReverseSide)
  707. != end(_specialScans));
  708. createSpecialScanRow(scan, info, requiresBothSides);
  709. scan.wrap->resizeToWidth(width());
  710. scan.row->show(anim::type::normal);
  711. if (scan.header) {
  712. scan.header->show(anim::type::normal);
  713. }
  714. specialScanChanged(type, true);
  715. }
  716. scan.file = std::move(info);
  717. }
  718. void EditScans::createSpecialScanRow(
  719. SpecialScan &scan,
  720. const ScanInfo &info,
  721. bool requiresBothSides) {
  722. Expects(scan.file.type != FileType::Scan
  723. && scan.file.type != FileType::Translation);
  724. const auto type = scan.file.type;
  725. const auto name = [&] {
  726. switch (type) {
  727. case FileType::FrontSide:
  728. return requiresBothSides
  729. ? tr::lng_passport_front_side_title(tr::now)
  730. : tr::lng_passport_main_page_title(tr::now);
  731. case FileType::ReverseSide:
  732. return tr::lng_passport_reverse_side_title(tr::now);
  733. case FileType::Selfie:
  734. return tr::lng_passport_selfie_title(tr::now);
  735. }
  736. Unexpected("Type in special file name.");
  737. }();
  738. scan.row = CreateScan(scan.wrap, info, name);
  739. const auto row = scan.row->entity();
  740. row->deleteClicks(
  741. ) | rpl::start_with_next([=] {
  742. _controller->deleteScan(type, std::nullopt);
  743. }, row->lifetime());
  744. row->restoreClicks(
  745. ) | rpl::start_with_next([=] {
  746. _controller->restoreScan(type, std::nullopt);
  747. }, row->lifetime());
  748. scan.rowCreated = !info.deleted;
  749. }
  750. void EditScans::chooseScan(FileType type) {
  751. if (!_controller->canAddScan(type)) {
  752. _controller->showToast(tr::lng_passport_scans_limit_reached(tr::now));
  753. return;
  754. }
  755. ChooseScan(this, type, [=](QByteArray &&content) {
  756. _controller->uploadScan(type, std::move(content));
  757. }, [=](ReadScanError error) {
  758. _controller->readScanError(error);
  759. });
  760. }
  761. void EditScans::ChooseScan(
  762. QPointer<QWidget> parent,
  763. FileType type,
  764. Fn<void(QByteArray&&)> doneCallback,
  765. Fn<void(ReadScanError)> errorCallback) {
  766. Expects(parent != nullptr);
  767. const auto filter = FileDialog::AllOrImagesFilter();
  768. const auto guardedCallback = crl::guard(parent, doneCallback);
  769. const auto guardedError = crl::guard(parent, errorCallback);
  770. const auto onMainError = [=](ReadScanError error) {
  771. crl::on_main([=] {
  772. guardedError(error);
  773. });
  774. };
  775. const auto processFiles = [=](
  776. QStringList &&files,
  777. const auto &handleImage) -> void {
  778. while (!files.isEmpty()) {
  779. auto file = files.front();
  780. files.removeAt(0);
  781. auto content = [&] {
  782. QFile f(file);
  783. if (f.size() > Images::kReadBytesLimit) {
  784. guardedError(ReadScanError::FileTooLarge);
  785. return QByteArray();
  786. } else if (!f.open(QIODevice::ReadOnly)) {
  787. guardedError(ReadScanError::CantReadImage);
  788. return QByteArray();
  789. }
  790. return f.readAll();
  791. }();
  792. if (!content.isEmpty()) {
  793. handleImage(
  794. std::move(content),
  795. std::move(files),
  796. handleImage);
  797. return;
  798. }
  799. }
  800. };
  801. const auto processImage = [=](
  802. QByteArray &&content,
  803. QStringList &&remainingFiles,
  804. const auto &repeatProcessImage) -> void {
  805. crl::async([
  806. =,
  807. bytes = std::move(content),
  808. remainingFiles = std::move(remainingFiles)
  809. ]() mutable {
  810. auto result = ProcessImage(std::move(bytes));
  811. if (const auto error = std::get_if<ReadScanError>(&result)) {
  812. onMainError(*error);
  813. } else {
  814. auto content = std::get_if<QByteArray>(&result);
  815. Assert(content != nullptr);
  816. crl::on_main([
  817. =,
  818. bytes = std::move(*content),
  819. remainingFiles = std::move(remainingFiles)
  820. ]() mutable {
  821. guardedCallback(std::move(bytes));
  822. processFiles(
  823. std::move(remainingFiles),
  824. repeatProcessImage);
  825. });
  826. }
  827. });
  828. };
  829. const auto processOpened = [=](FileDialog::OpenResult &&result) {
  830. if (result.paths.size() > 0) {
  831. processFiles(std::move(result.paths), processImage);
  832. } else if (!result.remoteContent.isEmpty()) {
  833. processImage(std::move(result.remoteContent), {}, processImage);
  834. }
  835. };
  836. const auto allowMany = (type == FileType::Scan)
  837. || (type == FileType::Translation);
  838. (allowMany ? FileDialog::GetOpenPaths : FileDialog::GetOpenPath)(
  839. parent,
  840. tr::lng_passport_choose_image(tr::now),
  841. filter,
  842. processOpened,
  843. nullptr);
  844. }
  845. void EditScans::hideSpecialScanError(FileType type) {
  846. toggleSpecialScanError(type, false);
  847. }
  848. void EditScans::specialScanChanged(FileType type, bool changed) {
  849. hideSpecialScanError(type);
  850. if (_specialScanChanged != changed) {
  851. _specialScanChanged = changed;
  852. updateErrorLabels();
  853. }
  854. }
  855. auto EditScans::findSpecialScan(FileType type) -> SpecialScan& {
  856. const auto i = _specialScans.find(type);
  857. Assert(i != end(_specialScans));
  858. return i->second;
  859. }
  860. void EditScans::toggleSpecialScanError(FileType type, bool shown) {
  861. auto &scan = findSpecialScan(type);
  862. if (scan.errorShown != shown) {
  863. scan.errorShown = shown;
  864. scan.errorAnimation.start(
  865. [=] { specialScanErrorAnimationCallback(type); },
  866. scan.errorShown ? 0. : 1.,
  867. scan.errorShown ? 1. : 0.,
  868. st::passportDetailsField.duration);
  869. }
  870. }
  871. void EditScans::specialScanErrorAnimationCallback(FileType type) {
  872. auto &scan = findSpecialScan(type);
  873. const auto error = scan.errorAnimation.value(
  874. scan.errorShown ? 1. : 0.);
  875. if (error == 0.) {
  876. scan.upload->setColorOverride(std::nullopt);
  877. } else {
  878. scan.upload->setColorOverride(anim::color(
  879. st::passportUploadButton.textFg,
  880. st::boxTextFgError,
  881. error));
  882. }
  883. }
  884. EditScans::~EditScans() = default;
  885. } // namespace Passport