Added ability to set profile photo from camera.

This commit is contained in:
23rd 2022-03-14 00:52:33 +03:00
parent 3b9ac19482
commit 3cb595c3c9
8 changed files with 297 additions and 189 deletions

View file

@ -1308,6 +1308,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_profile_migrate_learn_more" = "Learn more »";
"lng_profile_migrate_button" = "Upgrade to supergroup";
"lng_profile_add_more_after_create" = "You will be able to add more members after you create the group.";
"lng_profile_camera_title" = "Capture yourself";
"lng_channel_not_accessible" = "Sorry, this channel is not accessible.";
"lng_group_not_accessible" = "Sorry, this group is not accessible.";
@ -1459,6 +1460,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_attach_failed" = "Failed";
"lng_attach_file" = "File";
"lng_attach_photo" = "Photo";
"lng_attach_camera" = "Camera";
"lng_media_open_with" = "Open With";
"lng_media_download" = "Download";

View file

@ -67,7 +67,8 @@ void OpenWithPreparedFile(
void PrepareProfilePhoto(
not_null<Ui::RpWidget*> parent,
not_null<Window::Controller*> controller,
Fn<void(QImage &&image)> &&doneCallback) {
Fn<void(QImage &&image)> &&doneCallback,
QImage &&image) {
const auto resizeToMinSize = [=](
QImage &&image,
Qt::AspectRatioMode mode) {
@ -82,8 +83,53 @@ void PrepareProfilePhoto(
return std::move(image);
};
if (image.isNull()
|| (image.width() > (10 * image.height()))
|| (image.height() > (10 * image.width()))) {
controller->show(Ui::MakeInformBox(tr::lng_bad_photo()));
return;
}
image = resizeToMinSize(
std::move(image),
Qt::KeepAspectRatioByExpanding);
const auto fileImage = std::make_shared<Image>(std::move(image));
auto applyModifications = [=, done = std::move(doneCallback)](
const PhotoModifications &mods) {
done(resizeToMinSize(
ImageModified(fileImage->original(), mods),
Qt::KeepAspectRatio));
};
auto crop = [&] {
const auto &i = fileImage;
const auto minSide = std::min(i->width(), i->height());
return QRect(
(i->width() - minSide) / 2,
(i->height() - minSide) / 2,
minSide,
minSide);
}();
controller->showLayer(
std::make_unique<LayerWidget>(
parent,
controller,
fileImage,
PhotoModifications{ .crop = std::move(crop) },
std::move(applyModifications),
EditorData{
.cropType = EditorData::CropType::Ellipse,
.keepAspectRatio = true, }),
Ui::LayerOption::KeepOther);
}
void PrepareProfilePhotoFromFile(
not_null<Ui::RpWidget*> parent,
not_null<Window::Controller*> controller,
Fn<void(QImage &&image)> &&doneCallback) {
const auto callback = [=, done = std::move(doneCallback)](
const FileDialog::OpenResult &result) {
const FileDialog::OpenResult &result) mutable {
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
return;
}
@ -93,45 +139,11 @@ void PrepareProfilePhoto(
.content = result.remoteContent,
.forceOpaque = true,
}).image;
if (image.isNull()
|| (image.width() > (10 * image.height()))
|| (image.height() > (10 * image.width()))) {
controller->show(Ui::MakeInformBox(tr::lng_bad_photo()));
return;
}
image = resizeToMinSize(
std::move(image),
Qt::KeepAspectRatioByExpanding);
const auto fileImage = std::make_shared<Image>(std::move(image));
auto applyModifications = [=, done = std::move(done)](
const PhotoModifications &mods) {
done(resizeToMinSize(
ImageModified(fileImage->original(), mods),
Qt::KeepAspectRatio));
};
auto crop = [&] {
const auto &i = fileImage;
const auto minSide = std::min(i->width(), i->height());
return QRect(
(i->width() - minSide) / 2,
(i->height() - minSide) / 2,
minSide,
minSide);
}();
controller->showLayer(
std::make_unique<LayerWidget>(
parent,
controller,
fileImage,
PhotoModifications{ .crop = std::move(crop) },
std::move(applyModifications),
EditorData{
.cropType = EditorData::CropType::Ellipse,
.keepAspectRatio = true, }),
Ui::LayerOption::KeepOther);
PrepareProfilePhoto(
parent,
controller,
std::move(done),
std::move(image));
};
FileDialog::GetOpenPath(
parent.get(),

View file

@ -32,6 +32,12 @@ void OpenWithPreparedFile(
Fn<void()> &&doneCallback);
void PrepareProfilePhoto(
not_null<Ui::RpWidget*> parent,
not_null<Window::Controller*> controller,
Fn<void(QImage &&image)> &&doneCallback,
QImage &&image);
void PrepareProfilePhotoFromFile(
not_null<Ui::RpWidget*> parent,
not_null<Window::Controller*> controller,
Fn<void(QImage &&image)> &&doneCallback);

View file

@ -56,6 +56,155 @@ Calls::Calls(
Calls::~Calls() = default;
Webrtc::VideoTrack *Calls::AddCameraSubsection(
std::shared_ptr<Ui::Show> show,
not_null<Ui::VerticalLayout*> content,
bool saveToSettings) {
auto &lifetime = content->lifetime();
const auto hasCall = (Core::App().calls().currentCall() != nullptr);
const auto cameraNameStream = lifetime.make_state<
rpl::event_stream<QString>
>();
auto capturerOwner = lifetime.make_state<
std::shared_ptr<tgcalls::VideoCaptureInterface>
>();
const auto track = lifetime.make_state<VideoTrack>(
(hasCall
? VideoState::Inactive
: VideoState::Active));
const auto currentCameraName = [&] {
const auto cameras = GetVideoInputList();
const auto i = ranges::find(
cameras,
Core::App().settings().callVideoInputDeviceId(),
&VideoInput::id);
return (i != end(cameras))
? i->name
: tr::lng_settings_call_device_default(tr::now);
}();
AddButtonWithLabel(
content,
tr::lng_settings_call_input_device(),
rpl::single(
currentCameraName
) | rpl::then(
cameraNameStream->events()
),
st::settingsButtonNoIcon
)->addClickHandler([=] {
const auto &devices = GetVideoInputList();
const auto options = ranges::views::concat(
ranges::views::single(
tr::lng_settings_call_device_default(tr::now)),
devices | ranges::views::transform(&VideoInput::name)
) | ranges::to_vector;
const auto i = ranges::find(
devices,
Core::App().settings().callVideoInputDeviceId(),
&VideoInput::id);
const auto currentOption = (i != end(devices))
? int(i - begin(devices) + 1)
: 0;
const auto save = crl::guard(content, [=](int option) {
cameraNameStream->fire_copy(options[option]);
const auto deviceId = option
? devices[option - 1].id
: "default";
if (saveToSettings) {
Core::App().settings().setCallVideoInputDeviceId(deviceId);
Core::App().saveSettingsDelayed();
}
if (const auto call = Core::App().calls().currentCall()) {
call->setCurrentCameraDevice(deviceId);
}
if (*capturerOwner) {
(*capturerOwner)->switchToDevice(
deviceId.toStdString(),
false);
}
});
show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
SingleChoiceBox(box, {
.title = tr::lng_settings_call_camera(),
.options = options,
.initialSelection = currentOption,
.callback = save,
});
}));
});
const auto bubbleWrap = content->add(object_ptr<Ui::RpWidget>(content));
const auto bubble = lifetime.make_state<::Calls::VideoBubble>(
bubbleWrap,
track);
const auto padding = st::settingsButtonNoIcon.padding.left();
const auto top = st::boxRoundShadow.extend.top();
const auto bottom = st::boxRoundShadow.extend.bottom();
auto frameSize = track->renderNextFrame(
) | rpl::map([=] {
return track->frameSize();
}) | rpl::filter([=](QSize size) {
return !size.isEmpty()
&& !Core::App().calls().currentCall()
&& !Core::App().calls().currentGroupCall();
});
auto bubbleWidth = bubbleWrap->widthValue(
) | rpl::filter([=](int width) {
return width > 2 * padding + 1;
});
rpl::combine(
std::move(bubbleWidth),
std::move(frameSize)
) | rpl::start_with_next([=](int width, QSize frame) {
const auto useWidth = (width - 2 * padding);
const auto useHeight = std::min(
((useWidth * frame.height()) / frame.width()),
(useWidth * 480) / 640);
bubbleWrap->resize(width, top + useHeight + bottom);
bubble->updateGeometry(
::Calls::VideoBubble::DragMode::None,
QRect(padding, top, useWidth, useHeight));
bubbleWrap->update();
}, bubbleWrap->lifetime());
using namespace rpl::mappers;
const auto checkCapturer = [=] {
if (*capturerOwner
|| Core::App().calls().currentCall()
|| Core::App().calls().currentGroupCall()) {
return;
}
*capturerOwner = Core::App().calls().getVideoCapture(
Core::App().settings().callVideoInputDeviceId(),
false);
(*capturerOwner)->setPreferredAspectRatio(0.);
track->setState(VideoState::Active);
(*capturerOwner)->setState(tgcalls::VideoState::Active);
(*capturerOwner)->setOutput(track->sink());
};
rpl::combine(
Core::App().calls().currentCallValue(),
Core::App().calls().currentGroupCallValue(),
_1 || _2
) | rpl::start_with_next([=](bool has) {
if (has) {
track->setState(VideoState::Inactive);
bubbleWrap->resize(bubbleWrap->width(), 0);
*capturerOwner = nullptr;
} else {
crl::on_main(content, checkCapturer);
}
}, lifetime);
return track;
}
void Calls::sectionSaveChanges(FnMut<void()> done) {
if (_micTester) {
_micTester.reset();
@ -66,144 +215,13 @@ void Calls::sectionSaveChanges(FnMut<void()> done) {
void Calls::setupContent() {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
const auto &settings = Core::App().settings();
const auto cameras = GetVideoInputList();
if (!cameras.empty()) {
const auto hasCall = (Core::App().calls().currentCall() != nullptr);
auto capturerOwner = content->lifetime().make_state<
std::shared_ptr<tgcalls::VideoCaptureInterface>
>();
const auto track = content->lifetime().make_state<VideoTrack>(
(hasCall
? VideoState::Inactive
: VideoState::Active));
const auto currentCameraName = [&] {
const auto i = ranges::find(
cameras,
settings.callVideoInputDeviceId(),
&VideoInput::id);
return (i != end(cameras))
? i->name
: tr::lng_settings_call_device_default(tr::now);
}();
if (!GetVideoInputList().empty()) {
AddSkip(content);
AddSubsectionTitle(content, tr::lng_settings_call_camera());
AddButtonWithLabel(
AddCameraSubsection(
std::make_shared<Window::Show>(_controller),
content,
tr::lng_settings_call_input_device(),
rpl::single(
currentCameraName
) | rpl::then(
_cameraNameStream.events()
),
st::settingsButtonNoIcon
)->addClickHandler([=] {
const auto &devices = GetVideoInputList();
const auto options = ranges::views::concat(
ranges::views::single(
tr::lng_settings_call_device_default(tr::now)),
devices | ranges::views::transform(&VideoInput::name)
) | ranges::to_vector;
const auto i = ranges::find(
devices,
Core::App().settings().callVideoInputDeviceId(),
&VideoInput::id);
const auto currentOption = (i != end(devices))
? int(i - begin(devices) + 1)
: 0;
const auto save = crl::guard(this, [=](int option) {
_cameraNameStream.fire_copy(options[option]);
const auto deviceId = option
? devices[option - 1].id
: "default";
Core::App().settings().setCallVideoInputDeviceId(deviceId);
Core::App().saveSettingsDelayed();
if (const auto call = Core::App().calls().currentCall()) {
call->setCurrentCameraDevice(deviceId);
}
if (*capturerOwner) {
(*capturerOwner)->switchToDevice(
deviceId.toStdString(),
false);
}
});
_controller->show(Box([=](not_null<Ui::GenericBox*> box) {
SingleChoiceBox(box, {
.title = tr::lng_settings_call_camera(),
.options = options,
.initialSelection = currentOption,
.callback = save,
});
}));
});
const auto bubbleWrap = content->add(object_ptr<Ui::RpWidget>(content));
const auto bubble = content->lifetime().make_state<::Calls::VideoBubble>(
bubbleWrap,
track);
const auto padding = st::settingsButtonNoIcon.padding.left();
const auto top = st::boxRoundShadow.extend.top();
const auto bottom = st::boxRoundShadow.extend.bottom();
auto frameSize = track->renderNextFrame(
) | rpl::map([=] {
return track->frameSize();
}) | rpl::filter([=](QSize size) {
return !size.isEmpty()
&& !Core::App().calls().currentCall()
&& !Core::App().calls().currentGroupCall();
});
auto bubbleWidth = bubbleWrap->widthValue(
) | rpl::filter([=](int width) {
return width > 2 * padding + 1;
});
rpl::combine(
std::move(bubbleWidth),
std::move(frameSize)
) | rpl::start_with_next([=](int width, QSize frame) {
const auto useWidth = (width - 2 * padding);
const auto useHeight = std::min(
((useWidth * frame.height()) / frame.width()),
(useWidth * 480) / 640);
bubbleWrap->resize(width, top + useHeight + bottom);
bubble->updateGeometry(
::Calls::VideoBubble::DragMode::None,
QRect(padding, top, useWidth, useHeight));
bubbleWrap->update();
}, bubbleWrap->lifetime());
using namespace rpl::mappers;
const auto checkCapturer = [=] {
if (*capturerOwner
|| Core::App().calls().currentCall()
|| Core::App().calls().currentGroupCall()) {
return;
}
*capturerOwner = Core::App().calls().getVideoCapture(
Core::App().settings().callVideoInputDeviceId(),
false);
(*capturerOwner)->setPreferredAspectRatio(0.);
track->setState(VideoState::Active);
(*capturerOwner)->setState(tgcalls::VideoState::Active);
(*capturerOwner)->setOutput(track->sink());
};
rpl::combine(
Core::App().calls().currentCallValue(),
Core::App().calls().currentGroupCallValue(),
_1 || _2
) | rpl::start_with_next([=](bool has) {
if (has) {
track->setState(VideoState::Inactive);
bubbleWrap->resize(bubbleWrap->width(), 0);
*capturerOwner = nullptr;
} else {
crl::on_main(content, checkCapturer);
}
}, content->lifetime());
true);
AddSkip(content);
AddDivider(content);
}

View file

@ -23,10 +23,12 @@ class Call;
namespace Ui {
class LevelMeter;
class GenericBox;
class Show;
} // namespace Ui
namespace Webrtc {
class AudioInputTester;
class VideoTrack;
} // namespace Webrtc
namespace Settings {
@ -38,6 +40,11 @@ public:
void sectionSaveChanges(FnMut<void()> done) override;
static Webrtc::VideoTrack *AddCameraSubsection(
std::shared_ptr<Ui::Show> show,
not_null<Ui::VerticalLayout*> content,
bool saveToSettings);
private:
void setupContent();
void requestPermissionAndStartTestingMicrophone();

View file

@ -66,7 +66,7 @@ void SetupPhoto(
auto callback = [=](QImage &&image) {
self->session().api().peerPhoto().upload(self, std::move(image));
};
Editor::PrepareProfilePhoto(
Editor::PrepareProfilePhotoFromFile(
upload,
&controller->window(),
std::move(callback));

View file

@ -7,8 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/special_buttons.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "base/call_delayed.h"
#include "dialogs/ui/dialogs_layout.h"
#include "ui/effects/ripple_animation.h"
#include "ui/effects/radial_animation.h"
@ -32,19 +31,66 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_player.h"
#include "media/streaming/media_streaming_document.h"
#include "settings/settings_calls.h" // Calls::AddCameraSubsection.
#include "calls/calls_instance.h"
#include "webrtc/webrtc_media_devices.h" // Webrtc::GetVideoInputList.
#include "webrtc/webrtc_video_track.h"
#include "ui/widgets/popup_menu.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "mainwidget.h"
#include "facades.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
namespace Ui {
namespace {
constexpr auto kAnimationDuration = crl::time(120);
bool IsCameraAvailable() {
return (Core::App().calls().currentCall() == nullptr)
&& !Webrtc::GetVideoInputList().empty();
}
void CameraBox(
not_null<Ui::GenericBox*> box,
not_null<Window::Controller*> controller,
Fn<void(QImage &&image)> &&doneCallback) {
using namespace Webrtc;
const auto track = Settings::Calls::AddCameraSubsection(
std::make_shared<Ui::BoxShow>(box),
box->verticalLayout(),
false);
if (!track) {
box->closeBox();
return;
}
track->stateValue(
) | rpl::start_with_next([=](const VideoState &state) {
if (state == VideoState::Inactive) {
box->closeBox();
}
}, box->lifetime());
auto done = [=, done = std::move(doneCallback)](QImage &&image) {
box->closeBox();
done(std::move(image));
};
box->setTitle(tr::lng_profile_camera_title());
box->addButton(tr::lng_continue(), [=, done = std::move(done)]() mutable {
Editor::PrepareProfilePhoto(
box,
controller,
std::move(done),
track->frame(FrameRequest()).mirrored(true, false));
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
QString CropTitle(not_null<PeerData*> peer) {
if (peer->isChat() || peer->isMegagroup()) {
return tr::lng_create_group_crop(tr::now);
@ -198,10 +244,7 @@ void UserpicButton::prepare() {
void UserpicButton::setClickHandlerByRole() {
switch (_role) {
case Role::ChangePhoto:
addClickHandler(App::LambdaDelayed(
_st.changeButton.ripple.hideDuration,
this,
[=] { changePhotoLocally(); }));
addClickHandler([=] { changePhotoLocally(); });
break;
case Role::OpenPhoto:
@ -230,10 +273,26 @@ void UserpicButton::changePhotoLocally(bool requestToUpload) {
_uploadPhotoRequests.fire({});
}
};
Editor::PrepareProfilePhoto(
this,
_window,
std::move(callback));
const auto chooseFile = [=] {
base::call_delayed(
_st.changeButton.ripple.hideDuration,
crl::guard(this, [=] {
Editor::PrepareProfilePhotoFromFile(
this,
_window,
callback);
}));
};
if (!IsCameraAvailable()) {
chooseFile();
} else {
_menu = base::make_unique_q<Ui::PopupMenu>(this);
_menu->addAction(tr::lng_attach_file(tr::now), chooseFile);
_menu->addAction(tr::lng_attach_camera(tr::now), [=] {
_window->show(Box(CameraBox, _window, callback));
});
_menu->popup(QCursor::pos());
}
}
void UserpicButton::openPeerPhoto() {

View file

@ -36,6 +36,8 @@ struct Information;
namespace Ui {
class PopupMenu;
class HistoryDownButton : public RippleButton {
public:
HistoryDownButton(QWidget *parent, const style::TwoIconButton &st);
@ -158,6 +160,8 @@ private:
std::unique_ptr<Media::Streaming::Instance> _streamed;
PhotoData *_streamedPhoto = nullptr;
base::unique_qptr<Ui::PopupMenu> _menu;
bool _showSavedMessagesOnSelf = false;
bool _canOpenPhoto = false;
bool _cursorInChangeOverlay = false;