983 lines
		
	
	
	
		
			25 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			983 lines
		
	
	
	
		
			25 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 "passport/passport_panel_edit_scans.h"
 | 
						|
 | 
						|
#include "passport/passport_panel_controller.h"
 | 
						|
#include "passport/passport_panel_details_row.h"
 | 
						|
#include "info/profile/info_profile_button.h"
 | 
						|
#include "ui/widgets/buttons.h"
 | 
						|
#include "ui/widgets/labels.h"
 | 
						|
#include "ui/wrap/fade_wrap.h"
 | 
						|
#include "ui/wrap/slide_wrap.h"
 | 
						|
#include "ui/wrap/vertical_layout.h"
 | 
						|
#include "ui/text/text_utilities.h" // Ui::Text::ToUpper
 | 
						|
#include "ui/text_options.h"
 | 
						|
#include "core/file_utilities.h"
 | 
						|
#include "lang/lang_keys.h"
 | 
						|
#include "boxes/abstract_box.h"
 | 
						|
#include "storage/storage_media_prepare.h"
 | 
						|
#include "storage/file_upload.h" // For Storage::kUseBigFilesFrom.
 | 
						|
#include "styles/style_boxes.h"
 | 
						|
#include "styles/style_passport.h"
 | 
						|
 | 
						|
namespace Passport {
 | 
						|
namespace {
 | 
						|
 | 
						|
constexpr auto kMaxDimensions = 2048;
 | 
						|
constexpr auto kMaxSize = 10 * 1024 * 1024;
 | 
						|
constexpr auto kJpegQuality = 89;
 | 
						|
 | 
						|
static_assert(kMaxSize <= Storage::kUseBigFilesFrom);
 | 
						|
 | 
						|
base::variant<ReadScanError, QByteArray> ProcessImage(QByteArray &&bytes) {
 | 
						|
	auto image = App::readImage(base::take(bytes));
 | 
						|
	if (image.isNull()) {
 | 
						|
		return ReadScanError::CantReadImage;
 | 
						|
	} else if (!Storage::ValidateThumbDimensions(image.width(), image.height())) {
 | 
						|
		return ReadScanError::BadImageSize;
 | 
						|
	}
 | 
						|
	if (std::max(image.width(), image.height()) > kMaxDimensions) {
 | 
						|
		image = std::move(image).scaled(
 | 
						|
			kMaxDimensions,
 | 
						|
			kMaxDimensions,
 | 
						|
			Qt::KeepAspectRatio,
 | 
						|
			Qt::SmoothTransformation);
 | 
						|
	}
 | 
						|
	auto result = QByteArray();
 | 
						|
	{
 | 
						|
		QBuffer buffer(&result);
 | 
						|
		if (!image.save(&buffer, QByteArray("JPG"), kJpegQuality)) {
 | 
						|
			return ReadScanError::Unknown;
 | 
						|
		}
 | 
						|
		base::take(image);
 | 
						|
	}
 | 
						|
	if (result.isEmpty()) {
 | 
						|
		return ReadScanError::Unknown;
 | 
						|
	} else if (result.size() > kMaxSize) {
 | 
						|
		return ReadScanError::FileTooLarge;
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
} // namespace
 | 
						|
 | 
						|
class ScanButton : public Ui::AbstractButton {
 | 
						|
public:
 | 
						|
	ScanButton(
 | 
						|
		QWidget *parent,
 | 
						|
		const style::PassportScanRow &st,
 | 
						|
		const QString &name,
 | 
						|
		const QString &status,
 | 
						|
		bool deleted,
 | 
						|
		bool error);
 | 
						|
 | 
						|
	void setImage(const QImage &image);
 | 
						|
	void setStatus(const QString &status);
 | 
						|
	void setDeleted(bool deleted);
 | 
						|
	void setError(bool error);
 | 
						|
 | 
						|
	rpl::producer<> deleteClicks() const {
 | 
						|
		return _delete->entity()->clicks(
 | 
						|
		) | rpl::map([] { return rpl::empty_value(); });
 | 
						|
	}
 | 
						|
	rpl::producer<> restoreClicks() const {
 | 
						|
		return _restore->entity()->clicks(
 | 
						|
		) | rpl::map([] { return rpl::empty_value(); });
 | 
						|
	}
 | 
						|
 | 
						|
protected:
 | 
						|
	int resizeGetHeight(int newWidth) override;
 | 
						|
 | 
						|
	void paintEvent(QPaintEvent *e) override;
 | 
						|
 | 
						|
private:
 | 
						|
	int countAvailableWidth() const;
 | 
						|
 | 
						|
	const style::PassportScanRow &_st;
 | 
						|
	Ui::Text::String _name;
 | 
						|
	Ui::Text::String _status;
 | 
						|
	int _nameHeight = 0;
 | 
						|
	int _statusHeight = 0;
 | 
						|
	bool _error = false;
 | 
						|
	QImage _image;
 | 
						|
	object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> _delete;
 | 
						|
	object_ptr<Ui::FadeWrapScaled<Ui::RoundButton>> _restore;
 | 
						|
 | 
						|
};
 | 
						|
 | 
						|
struct EditScans::SpecialScan {
 | 
						|
	SpecialScan(ScanInfo &&file);
 | 
						|
 | 
						|
	ScanInfo file;
 | 
						|
	QPointer<Ui::SlideWrap<Ui::FlatLabel>> header;
 | 
						|
	QPointer<Ui::VerticalLayout> wrap;
 | 
						|
	base::unique_qptr<Ui::SlideWrap<ScanButton>> row;
 | 
						|
	QPointer<Info::Profile::Button> upload;
 | 
						|
	bool errorShown = false;
 | 
						|
	Ui::Animations::Simple errorAnimation;
 | 
						|
	rpl::variable<bool> rowCreated;
 | 
						|
};
 | 
						|
 | 
						|
void UpdateFileRow(
 | 
						|
		not_null<ScanButton*> button,
 | 
						|
		const ScanInfo &info) {
 | 
						|
	button->setStatus(info.status);
 | 
						|
	button->setImage(info.thumb);
 | 
						|
	button->setDeleted(info.deleted);
 | 
						|
	button->setError(!info.error.isEmpty());
 | 
						|
}
 | 
						|
 | 
						|
base::unique_qptr<Ui::SlideWrap<ScanButton>> CreateScan(
 | 
						|
		not_null<Ui::VerticalLayout*> parent,
 | 
						|
		const ScanInfo &info,
 | 
						|
		const QString &name) {
 | 
						|
	auto result = base::unique_qptr<Ui::SlideWrap<ScanButton>>(
 | 
						|
		parent->add(object_ptr<Ui::SlideWrap<ScanButton>>(
 | 
						|
			parent,
 | 
						|
			object_ptr<ScanButton>(
 | 
						|
				parent,
 | 
						|
				st::passportScanRow,
 | 
						|
				name,
 | 
						|
				info.status,
 | 
						|
				info.deleted,
 | 
						|
				!info.error.isEmpty()))));
 | 
						|
	result->entity()->setImage(info.thumb);
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
EditScans::List::List(
 | 
						|
	not_null<PanelController*> controller,
 | 
						|
	ScanListData &&data)
 | 
						|
: controller(controller)
 | 
						|
, files(std::move(data.files))
 | 
						|
, initialCount(int(files.size()))
 | 
						|
, errorMissing(data.errorMissing) {
 | 
						|
}
 | 
						|
 | 
						|
EditScans::List::List(
 | 
						|
	not_null<PanelController*> controller,
 | 
						|
	std::optional<ScanListData> &&data)
 | 
						|
: controller(controller)
 | 
						|
, files(data ? std::move(data->files) : std::vector<ScanInfo>())
 | 
						|
, initialCount(data ? base::make_optional(int(files.size())) : std::nullopt)
 | 
						|
, errorMissing(data ? std::move(data->errorMissing) : QString()) {
 | 
						|
}
 | 
						|
 | 
						|
bool EditScans::List::uploadedSomeMore() const {
 | 
						|
	if (!initialCount) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
	const auto from = begin(files) + *initialCount;
 | 
						|
	const auto till = end(files);
 | 
						|
	return std::find_if(from, till, [](const ScanInfo &file) {
 | 
						|
		return !file.deleted;
 | 
						|
	}) != till;
 | 
						|
}
 | 
						|
 | 
						|
bool EditScans::List::uploadMoreRequired() const {
 | 
						|
	if (!upload) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
	const auto exists = ranges::find_if(
 | 
						|
		files,
 | 
						|
		[](const ScanInfo &file) { return !file.deleted; }) != end(files);
 | 
						|
	if (!exists) {
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
	const auto errorExists = ranges::find_if(
 | 
						|
		files,
 | 
						|
		[](const ScanInfo &file) { return !file.error.isEmpty(); }
 | 
						|
	) != end(files);
 | 
						|
	return (errorExists || uploadMoreError) && !uploadedSomeMore();
 | 
						|
}
 | 
						|
 | 
						|
Ui::SlideWrap<ScanButton> *EditScans::List::nonDeletedErrorRow() const {
 | 
						|
	const auto nonDeletedErrorIt = ranges::find_if(
 | 
						|
		files,
 | 
						|
		[](const ScanInfo &file) {
 | 
						|
			return !file.error.isEmpty() && !file.deleted;
 | 
						|
		});
 | 
						|
	if (nonDeletedErrorIt == end(files)) {
 | 
						|
		return nullptr;
 | 
						|
	}
 | 
						|
	const auto index = (nonDeletedErrorIt - begin(files));
 | 
						|
	return rows[index].get();
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<QString> EditScans::List::uploadButtonText() const {
 | 
						|
	return (files.empty()
 | 
						|
		? tr::lng_passport_upload_scans
 | 
						|
		: tr::lng_passport_upload_more)() | Ui::Text::ToUpper();
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::List::hideError() {
 | 
						|
	toggleError(false);
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::List::toggleError(bool shown) {
 | 
						|
	if (errorShown != shown) {
 | 
						|
		errorShown = shown;
 | 
						|
		errorAnimation.start(
 | 
						|
			[=] { errorAnimationCallback(); },
 | 
						|
			errorShown ? 0. : 1.,
 | 
						|
			errorShown ? 1. : 0.,
 | 
						|
			st::passportDetailsField.duration);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::List::errorAnimationCallback() {
 | 
						|
	const auto error = errorAnimation.value(errorShown ? 1. : 0.);
 | 
						|
	if (error == 0.) {
 | 
						|
		upload->setColorOverride(std::nullopt);
 | 
						|
	} else {
 | 
						|
		upload->setColorOverride(anim::color(
 | 
						|
			st::passportUploadButton.textFg,
 | 
						|
			st::boxTextFgError,
 | 
						|
			error));
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::List::updateScan(ScanInfo &&info, int width) {
 | 
						|
	const auto i = ranges::find(files, info.key, [](const ScanInfo &file) {
 | 
						|
		return file.key;
 | 
						|
	});
 | 
						|
	if (i != files.end()) {
 | 
						|
		*i = std::move(info);
 | 
						|
		const auto scan = rows[i - files.begin()]->entity();
 | 
						|
		UpdateFileRow(scan, *i);
 | 
						|
		if (!i->deleted) {
 | 
						|
			hideError();
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		files.push_back(std::move(info));
 | 
						|
		pushScan(files.back());
 | 
						|
		wrap->resizeToWidth(width);
 | 
						|
		rows.back()->show(anim::type::normal);
 | 
						|
		if (divider) {
 | 
						|
			divider->hide(anim::type::normal);
 | 
						|
		}
 | 
						|
		header->show(anim::type::normal);
 | 
						|
		uploadTexts.fire(uploadButtonText());
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::List::pushScan(const ScanInfo &info) {
 | 
						|
	const auto index = rows.size();
 | 
						|
	const auto type = info.type;
 | 
						|
	rows.push_back(CreateScan(
 | 
						|
		wrap,
 | 
						|
		info,
 | 
						|
		tr::lng_passport_scan_index(tr::now, lt_index, QString::number(index + 1))));
 | 
						|
	rows.back()->hide(anim::type::instant);
 | 
						|
 | 
						|
	const auto scan = rows.back()->entity();
 | 
						|
 | 
						|
	scan->deleteClicks(
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
		controller->deleteScan(type, index);
 | 
						|
	}, scan->lifetime());
 | 
						|
 | 
						|
	scan->restoreClicks(
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
		controller->restoreScan(type, index);
 | 
						|
	}, scan->lifetime());
 | 
						|
 | 
						|
	hideError();
 | 
						|
}
 | 
						|
 | 
						|
ScanButton::ScanButton(
 | 
						|
	QWidget *parent,
 | 
						|
	const style::PassportScanRow &st,
 | 
						|
	const QString &name,
 | 
						|
	const QString &status,
 | 
						|
	bool deleted,
 | 
						|
	bool error)
 | 
						|
: AbstractButton(parent)
 | 
						|
, _st(st)
 | 
						|
, _name(
 | 
						|
	st::passportScanNameStyle,
 | 
						|
	name,
 | 
						|
	Ui::NameTextOptions())
 | 
						|
, _status(
 | 
						|
	st::defaultTextStyle,
 | 
						|
	status,
 | 
						|
	Ui::NameTextOptions())
 | 
						|
, _error(error)
 | 
						|
, _delete(this, object_ptr<Ui::IconButton>(this, _st.remove))
 | 
						|
, _restore(
 | 
						|
	this,
 | 
						|
	object_ptr<Ui::RoundButton>(
 | 
						|
		this,
 | 
						|
		tr::lng_passport_delete_scan_undo(),
 | 
						|
		_st.restore)) {
 | 
						|
	_delete->toggle(!deleted, anim::type::instant);
 | 
						|
	_restore->toggle(deleted, anim::type::instant);
 | 
						|
}
 | 
						|
 | 
						|
void ScanButton::setImage(const QImage &image) {
 | 
						|
	_image = image;
 | 
						|
	update();
 | 
						|
}
 | 
						|
 | 
						|
void ScanButton::setStatus(const QString &status) {
 | 
						|
	_status.setText(
 | 
						|
		st::defaultTextStyle,
 | 
						|
		status,
 | 
						|
		Ui::NameTextOptions());
 | 
						|
	update();
 | 
						|
}
 | 
						|
 | 
						|
void ScanButton::setDeleted(bool deleted) {
 | 
						|
	_delete->toggle(!deleted, anim::type::instant);
 | 
						|
	_restore->toggle(deleted, anim::type::instant);
 | 
						|
	update();
 | 
						|
}
 | 
						|
 | 
						|
void ScanButton::setError(bool error) {
 | 
						|
	_error = error;
 | 
						|
	update();
 | 
						|
}
 | 
						|
 | 
						|
int ScanButton::resizeGetHeight(int newWidth) {
 | 
						|
	_nameHeight = st::semiboldFont->height;
 | 
						|
	_statusHeight = st::normalFont->height;
 | 
						|
	const auto result = _st.padding.top() + _st.size + _st.padding.bottom();
 | 
						|
	const auto right = _st.padding.right();
 | 
						|
	_delete->moveToRight(
 | 
						|
		right,
 | 
						|
		(result - _delete->height()) / 2,
 | 
						|
		newWidth);
 | 
						|
	_restore->moveToRight(
 | 
						|
		right,
 | 
						|
		(result - _restore->height()) / 2,
 | 
						|
		newWidth);
 | 
						|
	return result + st::lineWidth;
 | 
						|
}
 | 
						|
 | 
						|
int ScanButton::countAvailableWidth() const {
 | 
						|
	return width()
 | 
						|
		- _st.padding.left()
 | 
						|
		- _st.textLeft
 | 
						|
		- _st.padding.right()
 | 
						|
		- std::max(_delete->width(), _restore->width());
 | 
						|
}
 | 
						|
 | 
						|
void ScanButton::paintEvent(QPaintEvent *e) {
 | 
						|
	Painter p(this);
 | 
						|
 | 
						|
	const auto left = _st.padding.left();
 | 
						|
	const auto top = _st.padding.top();
 | 
						|
	p.fillRect(
 | 
						|
		left,
 | 
						|
		height() - _st.border,
 | 
						|
		width() - left,
 | 
						|
		_st.border,
 | 
						|
		_st.borderFg);
 | 
						|
 | 
						|
	const auto deleted = _restore->toggled();
 | 
						|
	if (deleted) {
 | 
						|
		p.setOpacity(st::passportScanDeletedOpacity);
 | 
						|
	}
 | 
						|
 | 
						|
	if (_image.isNull()) {
 | 
						|
		p.fillRect(left, top, _st.size, _st.size, Qt::black);
 | 
						|
	} else {
 | 
						|
		PainterHighQualityEnabler hq(p);
 | 
						|
		const auto fromRect = [&] {
 | 
						|
			if (_image.width() > _image.height()) {
 | 
						|
				const auto shift = (_image.width() - _image.height()) / 2;
 | 
						|
				return QRect(shift, 0, _image.height(), _image.height());
 | 
						|
			} else {
 | 
						|
				const auto shift = (_image.height() - _image.width()) / 2;
 | 
						|
				return QRect(0, shift, _image.width(), _image.width());
 | 
						|
			}
 | 
						|
		}();
 | 
						|
		p.drawImage(QRect(left, top, _st.size, _st.size), _image, fromRect);
 | 
						|
	}
 | 
						|
	const auto availableWidth = countAvailableWidth();
 | 
						|
 | 
						|
	p.setPen(st::windowFg);
 | 
						|
	_name.drawLeftElided(
 | 
						|
		p,
 | 
						|
		left + _st.textLeft,
 | 
						|
		top + _st.nameTop,
 | 
						|
		availableWidth,
 | 
						|
		width());
 | 
						|
	p.setPen((_error && !deleted)
 | 
						|
		? st::boxTextFgError
 | 
						|
		: st::windowSubTextFg);
 | 
						|
	_status.drawLeftElided(
 | 
						|
		p,
 | 
						|
		left + _st.textLeft,
 | 
						|
		top + _st.statusTop,
 | 
						|
		availableWidth,
 | 
						|
		width());
 | 
						|
}
 | 
						|
 | 
						|
EditScans::SpecialScan::SpecialScan(ScanInfo &&file)
 | 
						|
: file(std::move(file)) {
 | 
						|
}
 | 
						|
 | 
						|
EditScans::EditScans(
 | 
						|
	QWidget *parent,
 | 
						|
	not_null<PanelController*> controller,
 | 
						|
	const QString &header,
 | 
						|
	const QString &error,
 | 
						|
	ScanListData &&scans,
 | 
						|
	std::optional<ScanListData> &&translations)
 | 
						|
: RpWidget(parent)
 | 
						|
, _controller(controller)
 | 
						|
, _error(error)
 | 
						|
, _content(this)
 | 
						|
, _scansList(_controller, std::move(scans))
 | 
						|
, _translationsList(_controller, std::move(translations)) {
 | 
						|
	setupScans(header);
 | 
						|
}
 | 
						|
 | 
						|
EditScans::EditScans(
 | 
						|
	QWidget *parent,
 | 
						|
	not_null<PanelController*> controller,
 | 
						|
	const QString &header,
 | 
						|
	const QString &error,
 | 
						|
	std::map<FileType, ScanInfo> &&specialFiles,
 | 
						|
	std::optional<ScanListData> &&translations)
 | 
						|
: RpWidget(parent)
 | 
						|
, _controller(controller)
 | 
						|
, _error(error)
 | 
						|
, _content(this)
 | 
						|
, _scansList(_controller)
 | 
						|
, _translationsList(_controller, std::move(translations)) {
 | 
						|
	setupSpecialScans(header, std::move(specialFiles));
 | 
						|
}
 | 
						|
 | 
						|
std::optional<int> EditScans::validateGetErrorTop() {
 | 
						|
	auto result = std::optional<int>();
 | 
						|
	const auto suggestResult = [&](int value) {
 | 
						|
		if (!result || *result > value) {
 | 
						|
			result = value;
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	if (_commonError && !somethingChanged()) {
 | 
						|
		suggestResult(_commonError->y());
 | 
						|
	}
 | 
						|
	const auto suggestList = [&](FileType type) {
 | 
						|
		auto &list = this->list(type);
 | 
						|
		if (list.uploadMoreRequired()) {
 | 
						|
			list.toggleError(true);
 | 
						|
			suggestResult((list.files.size() > 5)
 | 
						|
				? list.upload->y()
 | 
						|
				: list.header->y());
 | 
						|
		}
 | 
						|
		if (const auto row = list.nonDeletedErrorRow()) {
 | 
						|
			//toggleError(true);
 | 
						|
			suggestResult(row->y());
 | 
						|
		}
 | 
						|
	};
 | 
						|
	suggestList(FileType::Scan);
 | 
						|
	for (const auto &[type, scan] : _specialScans) {
 | 
						|
		if (!scan.file.key.id
 | 
						|
			|| scan.file.deleted
 | 
						|
			|| !scan.file.error.isEmpty()) {
 | 
						|
			toggleSpecialScanError(type, true);
 | 
						|
			suggestResult(scan.header ? scan.header->y() : scan.wrap->y());
 | 
						|
		}
 | 
						|
	}
 | 
						|
	suggestList(FileType::Translation);
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
EditScans::List &EditScans::list(FileType type) {
 | 
						|
	switch (type) {
 | 
						|
	case FileType::Scan: return _scansList;
 | 
						|
	case FileType::Translation: return _translationsList;
 | 
						|
	}
 | 
						|
	Unexpected("Type in EditScans::list().");
 | 
						|
}
 | 
						|
 | 
						|
const EditScans::List &EditScans::list(FileType type) const {
 | 
						|
	switch (type) {
 | 
						|
	case FileType::Scan: return _scansList;
 | 
						|
	case FileType::Translation: return _translationsList;
 | 
						|
	}
 | 
						|
	Unexpected("Type in EditScans::list() const.");
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::setupScans(const QString &header) {
 | 
						|
	const auto inner = _content.data();
 | 
						|
	inner->move(0, 0);
 | 
						|
 | 
						|
	if (!_error.isEmpty()) {
 | 
						|
		_commonError = inner->add(
 | 
						|
			object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
 | 
						|
				inner,
 | 
						|
				object_ptr<Ui::FlatLabel>(
 | 
						|
					inner,
 | 
						|
					_error,
 | 
						|
					st::passportVerifyErrorLabel),
 | 
						|
				st::passportValueErrorPadding));
 | 
						|
		_commonError->toggle(true, anim::type::instant);
 | 
						|
	}
 | 
						|
 | 
						|
	setupList(inner, FileType::Scan, header);
 | 
						|
	setupList(inner, FileType::Translation, tr::lng_passport_translation(tr::now));
 | 
						|
 | 
						|
	init();
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::setupList(
 | 
						|
		not_null<Ui::VerticalLayout*> container,
 | 
						|
		FileType type,
 | 
						|
		const QString &header) {
 | 
						|
	auto &list = this->list(type);
 | 
						|
	if (!list.initialCount) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	if (type == FileType::Scan) {
 | 
						|
		list.divider = container->add(
 | 
						|
			object_ptr<Ui::SlideWrap<BoxContentDivider>>(
 | 
						|
				container,
 | 
						|
				object_ptr<BoxContentDivider>(
 | 
						|
					container,
 | 
						|
					st::passportFormDividerHeight)));
 | 
						|
		list.divider->toggle(list.files.empty(), anim::type::instant);
 | 
						|
	}
 | 
						|
	list.header = container->add(
 | 
						|
		object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
 | 
						|
			container,
 | 
						|
			object_ptr<Ui::FlatLabel>(
 | 
						|
				container,
 | 
						|
				header,
 | 
						|
				st::passportFormHeader),
 | 
						|
			st::passportUploadHeaderPadding));
 | 
						|
	list.header->toggle(
 | 
						|
		!list.divider || !list.files.empty(),
 | 
						|
		anim::type::instant);
 | 
						|
	if (!list.errorMissing.isEmpty()) {
 | 
						|
		list.uploadMoreError = container->add(
 | 
						|
			object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
 | 
						|
				container,
 | 
						|
				object_ptr<Ui::FlatLabel>(
 | 
						|
					container,
 | 
						|
					list.errorMissing,
 | 
						|
					st::passportVerifyErrorLabel),
 | 
						|
				st::passportUploadErrorPadding));
 | 
						|
		list.uploadMoreError->toggle(true, anim::type::instant);
 | 
						|
	}
 | 
						|
	list.wrap = container->add(object_ptr<Ui::VerticalLayout>(container));
 | 
						|
	for (const auto &scan : list.files) {
 | 
						|
		list.pushScan(scan);
 | 
						|
		list.rows.back()->show(anim::type::instant);
 | 
						|
	}
 | 
						|
 | 
						|
	list.upload = container->add(
 | 
						|
		object_ptr<Info::Profile::Button>(
 | 
						|
			container,
 | 
						|
			list.uploadTexts.events_starting_with(
 | 
						|
				list.uploadButtonText()
 | 
						|
			) | rpl::flatten_latest(),
 | 
						|
			st::passportUploadButton),
 | 
						|
		st::passportUploadButtonPadding);
 | 
						|
	list.upload->addClickHandler([=] {
 | 
						|
		chooseScan(type);
 | 
						|
	});
 | 
						|
 | 
						|
	container->add(object_ptr<BoxContentDivider>(
 | 
						|
		container,
 | 
						|
		st::passportFormDividerHeight));
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::setupSpecialScans(
 | 
						|
		const QString &header,
 | 
						|
		std::map<FileType, ScanInfo> &&files) {
 | 
						|
	const auto requiresBothSides = files.find(FileType::ReverseSide)
 | 
						|
		!= end(files);
 | 
						|
	const auto title = [&](FileType type) {
 | 
						|
		switch (type) {
 | 
						|
		case FileType::FrontSide:
 | 
						|
			return requiresBothSides
 | 
						|
				? tr::lng_passport_front_side_title(tr::now)
 | 
						|
				: tr::lng_passport_main_page_title(tr::now);
 | 
						|
		case FileType::ReverseSide:
 | 
						|
			return tr::lng_passport_reverse_side_title(tr::now);
 | 
						|
		case FileType::Selfie:
 | 
						|
			return tr::lng_passport_selfie_title(tr::now);
 | 
						|
		}
 | 
						|
		Unexpected("Type in special row title.");
 | 
						|
	};
 | 
						|
	const auto uploadText = [=](FileType type, bool hasScan) {
 | 
						|
		switch (type) {
 | 
						|
		case FileType::FrontSide:
 | 
						|
			return requiresBothSides
 | 
						|
				? (hasScan
 | 
						|
					? tr::lng_passport_reupload_front_side
 | 
						|
					: tr::lng_passport_upload_front_side)
 | 
						|
				: (hasScan
 | 
						|
					? tr::lng_passport_reupload_main_page
 | 
						|
					: tr::lng_passport_upload_main_page);
 | 
						|
		case FileType::ReverseSide:
 | 
						|
			return hasScan
 | 
						|
				? tr::lng_passport_reupload_reverse_side
 | 
						|
				: tr::lng_passport_upload_reverse_side;
 | 
						|
		case FileType::Selfie:
 | 
						|
			return hasScan
 | 
						|
				? tr::lng_passport_reupload_selfie
 | 
						|
				: tr::lng_passport_upload_selfie;
 | 
						|
		}
 | 
						|
		Unexpected("Type in special row upload key.");
 | 
						|
	};
 | 
						|
	const auto description = [&](FileType type) {
 | 
						|
		switch (type) {
 | 
						|
		case FileType::FrontSide:
 | 
						|
			return requiresBothSides
 | 
						|
				? tr::lng_passport_front_side_description
 | 
						|
				: tr::lng_passport_main_page_description;
 | 
						|
		case FileType::ReverseSide:
 | 
						|
			return tr::lng_passport_reverse_side_description;
 | 
						|
		case FileType::Selfie:
 | 
						|
			return tr::lng_passport_selfie_description;
 | 
						|
		}
 | 
						|
		Unexpected("Type in special row upload key.");
 | 
						|
	};
 | 
						|
 | 
						|
	const auto inner = _content.data();
 | 
						|
	inner->move(0, 0);
 | 
						|
 | 
						|
	if (!_error.isEmpty()) {
 | 
						|
		_commonError = inner->add(
 | 
						|
			object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
 | 
						|
				inner,
 | 
						|
				object_ptr<Ui::FlatLabel>(
 | 
						|
					inner,
 | 
						|
					_error,
 | 
						|
					st::passportVerifyErrorLabel),
 | 
						|
				st::passportValueErrorPadding));
 | 
						|
		_commonError->toggle(true, anim::type::instant);
 | 
						|
	}
 | 
						|
 | 
						|
	for (auto &[type, info] : files) {
 | 
						|
		const auto i = _specialScans.emplace(
 | 
						|
			type,
 | 
						|
			SpecialScan(std::move(info))).first;
 | 
						|
		auto &scan = i->second;
 | 
						|
 | 
						|
		if (_specialScans.size() == 1) {
 | 
						|
			scan.header = inner->add(
 | 
						|
				object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
 | 
						|
					inner,
 | 
						|
					object_ptr<Ui::FlatLabel>(
 | 
						|
						inner,
 | 
						|
						header,
 | 
						|
						st::passportFormHeader),
 | 
						|
					st::passportUploadHeaderPadding));
 | 
						|
			scan.header->toggle(scan.file.key.id != 0, anim::type::instant);
 | 
						|
		}
 | 
						|
		scan.wrap = inner->add(object_ptr<Ui::VerticalLayout>(inner));
 | 
						|
		if (scan.file.key.id) {
 | 
						|
			createSpecialScanRow(scan, scan.file, requiresBothSides);
 | 
						|
		}
 | 
						|
		auto label = scan.rowCreated.value(
 | 
						|
		) | rpl::map([=, type = type](bool created) {
 | 
						|
			return uploadText(type, created)();
 | 
						|
		}) | rpl::flatten_latest(
 | 
						|
		) | Ui::Text::ToUpper();
 | 
						|
		scan.upload = inner->add(
 | 
						|
			object_ptr<Info::Profile::Button>(
 | 
						|
				inner,
 | 
						|
				std::move(label),
 | 
						|
				st::passportUploadButton),
 | 
						|
			st::passportUploadButtonPadding);
 | 
						|
		scan.upload->addClickHandler([=, type = type] {
 | 
						|
			chooseScan(type);
 | 
						|
		});
 | 
						|
 | 
						|
		inner->add(object_ptr<Ui::DividerLabel>(
 | 
						|
			inner,
 | 
						|
			object_ptr<Ui::FlatLabel>(
 | 
						|
				inner,
 | 
						|
				description(type)(tr::now),
 | 
						|
				st::boxDividerLabel),
 | 
						|
			st::passportFormLabelPadding));
 | 
						|
	}
 | 
						|
 | 
						|
	setupList(inner, FileType::Translation, tr::lng_passport_translation(tr::now));
 | 
						|
 | 
						|
	init();
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::init() {
 | 
						|
	_controller->scanUpdated(
 | 
						|
	) | rpl::start_with_next([=](ScanInfo &&info) {
 | 
						|
		updateScan(std::move(info));
 | 
						|
	}, lifetime());
 | 
						|
 | 
						|
	widthValue(
 | 
						|
	) | rpl::start_with_next([=](int width) {
 | 
						|
		_content->resizeToWidth(width);
 | 
						|
	}, _content->lifetime());
 | 
						|
 | 
						|
	_content->heightValue(
 | 
						|
	) | rpl::start_with_next([=](int height) {
 | 
						|
		resize(width(), height);
 | 
						|
	}, _content->lifetime());
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::updateScan(ScanInfo &&info) {
 | 
						|
	if (info.type != FileType::Scan && info.type != FileType::Translation) {
 | 
						|
		updateSpecialScan(std::move(info));
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	list(info.type).updateScan(std::move(info), width());
 | 
						|
	updateErrorLabels();
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::scanFieldsChanged(bool changed) {
 | 
						|
	if (_scanFieldsChanged != changed) {
 | 
						|
		_scanFieldsChanged = changed;
 | 
						|
		updateErrorLabels();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::updateErrorLabels() {
 | 
						|
	const auto updateList = [&](FileType type) {
 | 
						|
		auto &list = this->list(type);
 | 
						|
		if (list.uploadMoreError) {
 | 
						|
			list.uploadMoreError->toggle(
 | 
						|
				!list.uploadedSomeMore(),
 | 
						|
				anim::type::normal);
 | 
						|
		}
 | 
						|
	};
 | 
						|
	updateList(FileType::Scan);
 | 
						|
	updateList(FileType::Translation);
 | 
						|
	if (_commonError) {
 | 
						|
		_commonError->toggle(!somethingChanged(), anim::type::normal);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
bool EditScans::somethingChanged() const {
 | 
						|
	return list(FileType::Scan).uploadedSomeMore()
 | 
						|
		|| list(FileType::Translation).uploadedSomeMore()
 | 
						|
		|| _scanFieldsChanged
 | 
						|
		|| _specialScanChanged;
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::updateSpecialScan(ScanInfo &&info) {
 | 
						|
	Expects(info.key.id != 0);
 | 
						|
 | 
						|
	const auto type = info.type;
 | 
						|
	const auto i = _specialScans.find(type);
 | 
						|
	if (i == end(_specialScans)) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	auto &scan = i->second;
 | 
						|
	if (scan.file.key.id) {
 | 
						|
		UpdateFileRow(scan.row->entity(), info);
 | 
						|
		scan.rowCreated = !info.deleted;
 | 
						|
		if (scan.file.key.id != info.key.id) {
 | 
						|
			specialScanChanged(type, true);
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		const auto requiresBothSides
 | 
						|
			= (_specialScans.find(FileType::ReverseSide)
 | 
						|
				!= end(_specialScans));
 | 
						|
		createSpecialScanRow(scan, info, requiresBothSides);
 | 
						|
		scan.wrap->resizeToWidth(width());
 | 
						|
		scan.row->show(anim::type::normal);
 | 
						|
		if (scan.header) {
 | 
						|
			scan.header->show(anim::type::normal);
 | 
						|
		}
 | 
						|
		specialScanChanged(type, true);
 | 
						|
	}
 | 
						|
	scan.file = std::move(info);
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::createSpecialScanRow(
 | 
						|
		SpecialScan &scan,
 | 
						|
		const ScanInfo &info,
 | 
						|
		bool requiresBothSides) {
 | 
						|
	Expects(scan.file.type != FileType::Scan
 | 
						|
		&& scan.file.type != FileType::Translation);
 | 
						|
 | 
						|
	const auto type = scan.file.type;
 | 
						|
	const auto name = [&] {
 | 
						|
		switch (type) {
 | 
						|
		case FileType::FrontSide:
 | 
						|
			return requiresBothSides
 | 
						|
				? tr::lng_passport_front_side_title(tr::now)
 | 
						|
				: tr::lng_passport_main_page_title(tr::now);
 | 
						|
		case FileType::ReverseSide:
 | 
						|
			return tr::lng_passport_reverse_side_title(tr::now);
 | 
						|
		case FileType::Selfie:
 | 
						|
			return tr::lng_passport_selfie_title(tr::now);
 | 
						|
		}
 | 
						|
		Unexpected("Type in special file name.");
 | 
						|
	}();
 | 
						|
	scan.row = CreateScan(scan.wrap, info, name);
 | 
						|
	const auto row = scan.row->entity();
 | 
						|
 | 
						|
	row->deleteClicks(
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
		_controller->deleteScan(type, std::nullopt);
 | 
						|
	}, row->lifetime());
 | 
						|
 | 
						|
	row->restoreClicks(
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
		_controller->restoreScan(type, std::nullopt);
 | 
						|
	}, row->lifetime());
 | 
						|
 | 
						|
	scan.rowCreated = !info.deleted;
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::chooseScan(FileType type) {
 | 
						|
	if (!_controller->canAddScan(type)) {
 | 
						|
		_controller->showToast(tr::lng_passport_scans_limit_reached(tr::now));
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	ChooseScan(this, type, [=](QByteArray &&content) {
 | 
						|
		_controller->uploadScan(type, std::move(content));
 | 
						|
	}, [=](ReadScanError error) {
 | 
						|
		_controller->readScanError(error);
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::ChooseScan(
 | 
						|
		QPointer<QWidget> parent,
 | 
						|
		FileType type,
 | 
						|
		Fn<void(QByteArray&&)> doneCallback,
 | 
						|
		Fn<void(ReadScanError)> errorCallback) {
 | 
						|
	Expects(parent != nullptr);
 | 
						|
 | 
						|
	const auto processFiles = std::make_shared<Fn<void(QStringList&&)>>();
 | 
						|
	const auto filter = FileDialog::AllFilesFilter()
 | 
						|
		+ qsl(";;Image files (*")
 | 
						|
		+ cImgExtensions().join(qsl(" *"))
 | 
						|
		+ qsl(")");
 | 
						|
	const auto guardedCallback = crl::guard(parent, doneCallback);
 | 
						|
	const auto guardedError = crl::guard(parent, errorCallback);
 | 
						|
	const auto onMainCallback = [=](
 | 
						|
			QByteArray &&content,
 | 
						|
			QStringList &&remainingFiles) {
 | 
						|
		crl::on_main([
 | 
						|
			=,
 | 
						|
			bytes = std::move(content),
 | 
						|
			remainingFiles = std::move(remainingFiles)
 | 
						|
		]() mutable {
 | 
						|
			guardedCallback(std::move(bytes));
 | 
						|
			(*processFiles)(std::move(remainingFiles));
 | 
						|
		});
 | 
						|
	};
 | 
						|
	const auto onMainError = [=](ReadScanError error) {
 | 
						|
		crl::on_main([=] {
 | 
						|
			guardedError(error);
 | 
						|
		});
 | 
						|
	};
 | 
						|
	const auto processImage = [=](
 | 
						|
			QByteArray &&content,
 | 
						|
			QStringList &&remainingFiles) {
 | 
						|
		crl::async([
 | 
						|
			=,
 | 
						|
			bytes = std::move(content),
 | 
						|
			remainingFiles = std::move(remainingFiles)
 | 
						|
		]() mutable {
 | 
						|
			auto result = ProcessImage(std::move(bytes));
 | 
						|
			if (const auto error = base::get_if<ReadScanError>(&result)) {
 | 
						|
				onMainError(*error);
 | 
						|
			} else {
 | 
						|
				auto content = base::get_if<QByteArray>(&result);
 | 
						|
				Assert(content != nullptr);
 | 
						|
				onMainCallback(std::move(*content), std::move(remainingFiles));
 | 
						|
			}
 | 
						|
		});
 | 
						|
	};
 | 
						|
	const auto processOpened = [=](FileDialog::OpenResult &&result) {
 | 
						|
		if (result.paths.size() > 0) {
 | 
						|
			(*processFiles)(std::move(result.paths));
 | 
						|
		} else if (!result.remoteContent.isEmpty()) {
 | 
						|
			processImage(std::move(result.remoteContent), {});
 | 
						|
		}
 | 
						|
	};
 | 
						|
	*processFiles = [=](QStringList &&files) {
 | 
						|
		while (!files.isEmpty()) {
 | 
						|
			auto file = files.front();
 | 
						|
			files.removeAt(0);
 | 
						|
 | 
						|
			auto content = [&] {
 | 
						|
				QFile f(file);
 | 
						|
				if (f.size() > App::kImageSizeLimit) {
 | 
						|
					guardedError(ReadScanError::FileTooLarge);
 | 
						|
					return QByteArray();
 | 
						|
				} else if (!f.open(QIODevice::ReadOnly)) {
 | 
						|
					guardedError(ReadScanError::CantReadImage);
 | 
						|
					return QByteArray();
 | 
						|
				}
 | 
						|
				return f.readAll();
 | 
						|
			}();
 | 
						|
			if (!content.isEmpty()) {
 | 
						|
				processImage(std::move(content), std::move(files));
 | 
						|
				return;
 | 
						|
			}
 | 
						|
		}
 | 
						|
	};
 | 
						|
	const auto allowMany = (type == FileType::Scan)
 | 
						|
		|| (type == FileType::Translation);
 | 
						|
	(allowMany ? FileDialog::GetOpenPaths : FileDialog::GetOpenPath)(
 | 
						|
		parent,
 | 
						|
		tr::lng_passport_choose_image(tr::now),
 | 
						|
		filter,
 | 
						|
		processOpened,
 | 
						|
		nullptr);
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::hideSpecialScanError(FileType type) {
 | 
						|
	toggleSpecialScanError(type, false);
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::specialScanChanged(FileType type, bool changed) {
 | 
						|
	hideSpecialScanError(type);
 | 
						|
	if (_specialScanChanged != changed) {
 | 
						|
		_specialScanChanged = changed;
 | 
						|
		updateErrorLabels();
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
auto EditScans::findSpecialScan(FileType type) -> SpecialScan& {
 | 
						|
	const auto i = _specialScans.find(type);
 | 
						|
	Assert(i != end(_specialScans));
 | 
						|
	return i->second;
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::toggleSpecialScanError(FileType type, bool shown) {
 | 
						|
	auto &scan = findSpecialScan(type);
 | 
						|
	if (scan.errorShown != shown) {
 | 
						|
		scan.errorShown = shown;
 | 
						|
		scan.errorAnimation.start(
 | 
						|
			[=] { specialScanErrorAnimationCallback(type); },
 | 
						|
			scan.errorShown ? 0. : 1.,
 | 
						|
			scan.errorShown ? 1. : 0.,
 | 
						|
			st::passportDetailsField.duration);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void EditScans::specialScanErrorAnimationCallback(FileType type) {
 | 
						|
	auto &scan = findSpecialScan(type);
 | 
						|
	const auto error = scan.errorAnimation.value(
 | 
						|
		scan.errorShown ? 1. : 0.);
 | 
						|
	if (error == 0.) {
 | 
						|
		scan.upload->setColorOverride(std::nullopt);
 | 
						|
	} else {
 | 
						|
		scan.upload->setColorOverride(anim::color(
 | 
						|
			st::passportUploadButton.textFg,
 | 
						|
			st::boxTextFgError,
 | 
						|
			error));
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
EditScans::~EditScans() = default;
 | 
						|
 | 
						|
} // namespace Passport
 |