From 2119dae0b5738825a7dfa764baf3ca0351fff255 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 2 Apr 2026 12:17:58 +0300 Subject: [PATCH] Added simple radial upload progress overlay to userpic widgets. --- Telegram/SourceFiles/api/api_peer_photo.cpp | 125 +++++++++-- Telegram/SourceFiles/api/api_peer_photo.h | 27 +++ Telegram/SourceFiles/boxes/boxes.style | 5 + .../info/profile/info_profile_top_bar.cpp | 61 +++++- .../info/profile/info_profile_top_bar.h | 5 + .../sections/settings_information.cpp | 4 + .../settings/sections/settings_main.cpp | 4 + .../ui/controls/userpic_button.cpp | 72 ++++++- .../SourceFiles/ui/controls/userpic_button.h | 7 + .../ui/effects/upload_progress_overlay.cpp | 198 ++++++++++++++++++ .../ui/effects/upload_progress_overlay.h | 60 ++++++ Telegram/cmake/td_ui.cmake | 2 + 12 files changed, 549 insertions(+), 21 deletions(-) create mode 100644 Telegram/SourceFiles/ui/effects/upload_progress_overlay.cpp create mode 100644 Telegram/SourceFiles/ui/effects/upload_progress_overlay.h diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp index 0bee8ef7bb..ebfe6df9e4 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.cpp +++ b/Telegram/SourceFiles/api/api_peer_photo.cpp @@ -150,12 +150,37 @@ PeerPhoto::PeerPhoto(not_null api) : _session(&api->session()) , _api(&api->instance()) { crl::on_main(_session, [=] { + auto &uploader = _session->uploader(); + // You can't use _session->lifetime() in the constructor, // only queued, because it is not constructed yet. - _session->uploader().photoReady( + uploader.photoReady( ) | rpl::on_next([=](const Storage::UploadedMedia &data) { ready(data.fullId, data.info.file, std::nullopt); }, _session->lifetime()); + + uploader.photoProgress( + ) | rpl::on_next([=](const FullMsgId &id) { + const auto i = _uploads.find(id); + if (i == end(_uploads) || !i->second.photoId) { + return; + } + const auto peer = i->second.peer; + const auto photo = _session->data().photo( + i->second.photoId); + _uploadProgress.fire({ peer, photo->progress() }); + }, _session->lifetime()); + + uploader.photoFailed( + ) | rpl::on_next([=](const FullMsgId &id) { + const auto i = _uploads.find(id); + if (i == end(_uploads)) { + return; + } + const auto peer = i->second.peer; + _uploads.erase(i); + _uploadFailed.fire_copy(peer); + }, _session->lifetime()); }); } @@ -222,17 +247,18 @@ void PeerPhoto::upload( _session->uploader().cancel(already->first); _uploads.erase(already); } - _uploads.emplace( + const auto &[it, ok] = _uploads.emplace( fakeId, - UploadValue{ peer, type, std::move(done) }); + UploadValue{ peer, type, std::move(done), PhotoId(0) }); if (mtpMarkup) { ready(fakeId, std::nullopt, mtpMarkup); } else { - const auto ready = PreparePeerPhoto( + const auto prepared = PreparePeerPhoto( _api.instance().mainDcId(), peer->id, base::take(photo.image)); - _session->uploader().upload(fakeId, ready); + it->second.photoId = prepared->thumbId; + _session->uploader().upload(fakeId, prepared); } } @@ -240,6 +266,68 @@ void PeerPhoto::suggest(not_null peer, UserPhoto &&photo) { upload(peer, std::move(photo), UploadType::Suggestion, nullptr); } +void PeerPhoto::subscribeToUpload( + not_null peer, + rpl::lifetime &lifetime, + UploadCallbacks callbacks) { + uploadProgress( + ) | rpl::filter([=](const UploadProgress &data) { + return (data.peer == peer); + }) | rpl::on_next([cb = callbacks.progress](const UploadProgress &data) { + if (cb) { + cb(data.progress); + } + }, lifetime); + + uploadDone( + ) | rpl::filter([=](not_null p) { + return (p == peer); + }) | rpl::on_next([cb = callbacks.done](not_null) { + if (cb) { + cb(); + } + }, lifetime); + + uploadFailed( + ) | rpl::filter([=](not_null p) { + return (p == peer); + }) | rpl::on_next([cb = callbacks.failed](not_null) { + if (cb) { + cb(); + } + }, lifetime); +} + +auto PeerPhoto::uploadProgress() const +-> rpl::producer { + return _uploadProgress.events(); +} + +auto PeerPhoto::uploadDone() const +-> rpl::producer> { + return _uploadDone.events(); +} + +auto PeerPhoto::uploadFailed() const +-> rpl::producer> { + return _uploadFailed.events(); +} + +void PeerPhoto::cancelUpload(not_null peer) { + peer = peer->migrateToOrMe(); + const auto i = ranges::find( + _uploads, + peer, + [](const auto &pair) { return pair.second.peer; }); + if (i == end(_uploads)) { + return; + } + const auto fakeId = i->first; + _uploads.erase(i); + _session->uploader().cancel(fakeId); + _uploadFailed.fire_copy(peer); +} + void PeerPhoto::clear(not_null photo) { const auto self = _session->user(); if (self->userpicPhotoId() == photo->id) { @@ -362,12 +450,19 @@ void PeerPhoto::ready( const auto peer = maybeUploadValue->peer; const auto type = maybeUploadValue->type; const auto done = maybeUploadValue->done; - const auto applier = [=](const MTPUpdates &result) { - _session->updates().applyUpdates(result); + const auto finish = [=] { + _uploadDone.fire_copy(peer); if (done) { done(); } }; + const auto fail = [=](const MTP::Error &error) { + _uploadFailed.fire_copy(peer); + }; + const auto applier = [=](const MTPUpdates &result) { + _session->updates().applyUpdates(result); + finish(); + }; const auto botUserInput = [&] { const auto user = peer->asUser(); return (user && user->botInfo && user->botInfo->canEditInformation) @@ -400,10 +495,8 @@ void PeerPhoto::ready( peerToUser(peer->id), photoId)); } - if (done) { - done(); - } - }).send(); + finish(); + }).fail(fail).send(); } else if (const auto chat = peer->asChat()) { const auto history = _session->data().history(chat); using Flag = MTPDinputChatUploadedPhoto::Flag; @@ -417,7 +510,7 @@ void PeerPhoto::ready( MTPInputFile(), // video MTPdouble(), // video_start_ts videoSize ? (*videoSize) : MTPVideoSize()) // video_emoji_markup - )).done(applier).afterRequest(history->sendRequestId).send(); + )).done(applier).fail(fail).afterRequest(history->sendRequestId).send(); } else if (const auto channel = peer->asChannel()) { using Flag = MTPDinputChatUploadedPhoto::Flag; const auto none = MTPDinputChatUploadedPhoto::Flags(0); @@ -431,7 +524,7 @@ void PeerPhoto::ready( MTPInputFile(), // video MTPdouble(), // video_start_ts videoSize ? (*videoSize) : MTPVideoSize()) // video_emoji_markup - )).done(applier).afterRequest(history->sendRequestId).send(); + )).done(applier).fail(fail).afterRequest(history->sendRequestId).send(); } else if (const auto user = peer->asUser()) { using Flag = MTPphotos_UploadContactProfilePhoto::Flag; const auto none = MTPphotos_UploadContactProfilePhoto::Flags(0); @@ -454,10 +547,8 @@ void PeerPhoto::ready( if (type != UploadType::Suggestion) { user->updateFullForced(); } - if (done) { - done(); - } - }).send(); + finish(); + }).fail(fail).send(); } } diff --git a/Telegram/SourceFiles/api/api_peer_photo.h b/Telegram/SourceFiles/api/api_peer_photo.h index 71d340a031..4f417f930e 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.h +++ b/Telegram/SourceFiles/api/api_peer_photo.h @@ -41,6 +41,11 @@ public: std::vector markupColors; }; + struct UploadProgress { + not_null peer; + float64 progress = 0.; + }; + void upload( not_null peer, UserPhoto &&photo, @@ -55,6 +60,24 @@ public: void clearPersonal(not_null user); void set(not_null peer, not_null photo); + struct UploadCallbacks { + Fn progress; + Fn done; + Fn failed; + }; + void subscribeToUpload( + not_null peer, + rpl::lifetime &lifetime, + UploadCallbacks callbacks); + + [[nodiscard]] auto uploadProgress() const + -> rpl::producer; + [[nodiscard]] auto uploadDone() const + -> rpl::producer>; + [[nodiscard]] auto uploadFailed() const + -> rpl::producer>; + void cancelUpload(not_null peer); + void requestUserPhotos(not_null user, UserPhotoId afterId); void requestEmojiList(EmojiListType type); @@ -100,9 +123,13 @@ private: not_null peer; UploadType type = UploadType::Default; Fn done; + PhotoId photoId = 0; }; base::flat_map _uploads; + rpl::event_stream _uploadProgress; + rpl::event_stream> _uploadDone; + rpl::event_stream> _uploadFailed; base::flat_map, mtpRequestId> _userPhotosRequests; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 1e0cdfa0d7..23e34ce706 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -23,6 +23,8 @@ UserpicButton { uploadBg: color; uploadIcon: icon; uploadIconPosition: point; + uploadProgressLine: pixels; + uploadProgressMargin: pixels; } UserpicsRow { button: UserpicButton; @@ -72,6 +74,8 @@ defaultUserpicButton: UserpicButton { uploadBg: msgDateImgBgOver; uploadIcon: defaultUploadUserpicIcon; uploadIconPosition: point(-1px, 1px); + uploadProgressLine: 3px; + uploadProgressMargin: 8px; } uploadUserpicSize: 32px; uploadUserpicButton: UserpicButton(defaultUserpicButton) { @@ -81,6 +85,7 @@ uploadUserpicButton: UserpicButton(defaultUserpicButton) { changeIconPosition: point(4px, 4px); } uploadUserpicButtonBorder: 2px; +userpicUploadCancel: icon {{ "history_file_cancel", historyFileThumbIconFg }}; restoreUserpicIcon: UserpicButton(defaultUserpicButton) { size: size(22px, 22px); photoSize: 22px; diff --git a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp index fbde7a01c2..ba6f2fdc66 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp @@ -70,6 +70,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/stars_rating.h" #include "ui/controls/userpic_button.h" #include "ui/effects/animations.h" +#include "ui/effects/upload_progress_overlay.h" #include "ui/effects/outline_segments.h" #include "ui/effects/round_checkbox.h" #include "ui/empty_userpic.h" @@ -1120,6 +1121,7 @@ void TopBar::setupUserpicButton( _peer->session().api().peerPhoto().upload( _peer, std::move(result)); + startUploadOverlay(); break; case ChosenType::Suggest: _peer->session().api().peerPhoto().suggest( @@ -1280,7 +1282,9 @@ void TopBar::setupUserpicButton( (*menu)->popup(QCursor::pos()); } } else if (button == Qt::LeftButton) { - if (_topicIconView && _topic && _topic->iconId()) { + if (_uploadOverlay && _uploadOverlay->uploading()) { + _peer->session().api().peerPhoto().cancelUpload(_peer); + } else if (_topicIconView && _topic && _topic->iconId()) { const auto document = _peer->owner().document( _topic->iconId()); if (const auto sticker = document->sticker()) { @@ -1321,6 +1325,46 @@ void TopBar::setupUserpicButton( }, _userpicButton->lifetime()); } +void TopBar::startUploadOverlay() { + if (_uploadOverlay && _uploadOverlay->uploading()) { + return; + } + _waitingUserpicCloudLoad = true; + _uploadOverlay = std::make_unique( + this, + [=] { update(); }); + _uploadOverlay->start(); + + _userpicButton->events( + ) | rpl::filter([](not_null e) { + return e->type() == QEvent::Enter + || e->type() == QEvent::Leave; + }) | rpl::on_next([=](not_null e) { + if (_uploadOverlay) { + _uploadOverlay->setOver(e->type() == QEvent::Enter); + } + }, _uploadLifetime); + + const auto cleanup = [=] { + _uploadLifetime.destroy(); + _uploadOverlay = nullptr; + }; + _peer->session().api().peerPhoto().subscribeToUpload( + _peer, + _uploadLifetime, + { + .progress = [=](float64 value) { + _uploadOverlay->setProgress(value); + }, + .done = [=] { + _uploadOverlay->stop(cleanup); + }, + .failed = [=] { + _uploadOverlay->fail(cleanup); + }, + }); +} + void TopBar::setupUniqueBadgeTooltip() { if (!_badge || _source == Source::Preview) { return; @@ -1738,7 +1782,11 @@ void TopBar::paintUserpic(QPainter &p, const QRect &geometry) { } } const auto key = _peer->userpicUniqueKey(_userpicView); - if (_userpicUniqueKey != key) { + const auto overlayActive = _uploadOverlay && _uploadOverlay->shown(); + const auto awaitingCloud = _waitingUserpicCloudLoad + && !_peer->userpicCloudImage(_userpicView); + if (!overlayActive && !awaitingCloud && _userpicUniqueKey != key) { + _waitingUserpicCloudLoad = false; _userpicUniqueKey = key; const auto fullSize = st::infoProfileTopBarPhotoSize; const auto scaled = fullSize * style::DevicePixelRatio(); @@ -1773,6 +1821,15 @@ void TopBar::paintUserpic(QPainter &p, const QRect &geometry) { _cachedUserpic.setDevicePixelRatio(style::DevicePixelRatio()); } p.drawImage(geometry, _cachedUserpic); + if (_uploadOverlay && _uploadOverlay->shown()) { + _uploadOverlay->paint(p, geometry, { + .lineWidth = st::defaultUserpicButton.uploadProgressLine, + .margin = st::defaultUserpicButton.uploadProgressMargin, + .progressFg = st::historyFileThumbRadialFg, + .overlayFg = st::songCoverOverlayFg, + .cancelIcon = &st::userpicUploadCancel, + }); + } } void TopBar::paintEvent(QPaintEvent *e) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_top_bar.h b/Telegram/SourceFiles/info/profile/info_profile_top_bar.h index 69e9b6bb9a..201f90e675 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_top_bar.h +++ b/Telegram/SourceFiles/info/profile/info_profile_top_bar.h @@ -32,6 +32,7 @@ class MultiPlayer; } // namespace Lottie namespace Ui { +class UploadProgressOverlay; class VideoUserpicPlayer; struct OutlineSegment; namespace Text { @@ -151,6 +152,7 @@ private: not_null controller, const Ui::Menu::MenuCallback &addAction); void setupUserpicButton(not_null controller); + void startUploadOverlay(); void setupActions(not_null controller); void setupButtons( not_null controller, @@ -252,6 +254,9 @@ private: QImage _monoforumMask; std::unique_ptr _videoUserpicPlayer; std::unique_ptr _topicIconView; + std::unique_ptr _uploadOverlay; + rpl::lifetime _uploadLifetime; + bool _waitingUserpicCloudLoad = false; rpl::lifetime _userpicLoadingLifetime; base::unique_qptr _close; diff --git a/Telegram/SourceFiles/settings/sections/settings_information.cpp b/Telegram/SourceFiles/settings/sections/settings_information.cpp index db8a54fc45..0652f238a2 100644 --- a/Telegram/SourceFiles/settings/sections/settings_information.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_information.cpp @@ -283,6 +283,7 @@ void SetupPhoto( auto &image = chosen.image; UpdatePhotoLocally(self, image); photo->showCustom(base::duplicate(image)); + const auto isMarkup = (chosen.markup.documentId != 0); self->session().api().peerPhoto().upload( self, { @@ -290,6 +291,9 @@ void SetupPhoto( chosen.markup.documentId, chosen.markup.colors, }); + if (!isMarkup) { + photo->showUploadProgress(); + } }, upload->lifetime()); const auto name = Ui::CreateChild( diff --git a/Telegram/SourceFiles/settings/sections/settings_main.cpp b/Telegram/SourceFiles/settings/sections/settings_main.cpp index da9822a5cd..b8d8bb7fcd 100644 --- a/Telegram/SourceFiles/settings/sections/settings_main.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_main.cpp @@ -188,6 +188,7 @@ Cover::Cover( Ui::UserpicButton::ChosenImage chosen) { auto &image = chosen.image; _userpic->showCustom(base::duplicate(image)); + const auto isMarkup = (chosen.markup.documentId != 0); _user->session().api().peerPhoto().upload( _user, { @@ -195,6 +196,9 @@ Cover::Cover( chosen.markup.documentId, chosen.markup.colors, }); + if (!isMarkup) { + _userpic->showUploadProgress(); + } }); _badge.setPremiumClickCallback([=] { diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index 5a0a42c03b..b114c15cfe 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/userpic_button.h" #include "apiwrap.h" +#include "api/api_peer_photo.h" +#include "ui/effects/upload_progress_overlay.h" #include "api/api_user_privacy.h" #include "base/call_delayed.h" #include "boxes/edit_privacy_box.h" @@ -30,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/widgets/menu/menu_action.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/ui_utility.h" #include "editor/photo_editor_common.h" #include "editor/photo_editor_layer_widget.h" @@ -294,6 +297,10 @@ void UserpicButton::setClickHandlerByRole() { } void UserpicButton::choosePhotoLocally() { + if (_uploadOverlay && _uploadOverlay->uploading()) { + _peer->session().api().peerPhoto().cancelUpload(_peer); + return; + } if (!_window) { return; } else if (const auto controller = _window->sessionController()) { @@ -499,6 +506,10 @@ void UserpicButton::openPeerPhoto() { Expects(_peer != nullptr); Expects(_controller != nullptr); + if (_uploadOverlay && _uploadOverlay->uploading()) { + _peer->session().api().peerPhoto().cancelUpload(_peer); + return; + } if (_changeOverlayEnabled && _cursorInChangeOverlay) { choosePhotoLocally(); return; @@ -612,7 +623,10 @@ void UserpicButton::paintEvent(QPaintEvent *e) { p.translate(-photoLeft, -photoTop); }; - if (_role == Role::ChangePhoto || _role == Role::ChoosePhoto) { + const auto uploadShown = _uploadOverlay + && _uploadOverlay->shown(); + if (!uploadShown + && (_role == Role::ChangePhoto || _role == Role::ChoosePhoto)) { auto over = isOver() || isDown(); if (over) { fillTranslatedShape(_userpicHasImage @@ -633,7 +647,7 @@ void UserpicButton::paintEvent(QPaintEvent *e) { photoTop + iconTop, width()); } - } else if (_changeOverlayEnabled) { + } else if (!uploadShown && _changeOverlayEnabled) { auto current = _changeOverlayShown.value( (isOver() || isDown()) ? 1. : 0.); auto barHeight = anim::interpolate( @@ -665,6 +679,18 @@ void UserpicButton::paintEvent(QPaintEvent *e) { } } } + if (uploadShown) { + _uploadOverlay->paint(p, QRect(photoPosition, Size(_st.photoSize)), { + .lineWidth = _st.uploadProgressLine, + .margin = _st.uploadProgressMargin, + .progressFg = st::historyFileThumbRadialFg, + .overlayFg = st::songCoverOverlayFg, + .cancelIcon = &st::userpicUploadCancel, + .roundRadius = useForumShape() + ? (_st.photoSize * ForumUserpicRadiusMultiplier()) + : 0., + }); + } } void UserpicButton::paintUserpicFrame(Painter &p, QPoint photoPosition) { @@ -1055,6 +1081,14 @@ void UserpicButton::onStateChanged( startChangeOverlayAnimation(); } } + if (_uploadOverlay && _uploadOverlay->uploading()) { + const auto over = isOver() || isDown(); + const auto wasOver = (was & StateFlag::Over) + || (was & StateFlag::Down); + if (over != wasOver) { + _uploadOverlay->setOver(over); + } + } } void UserpicButton::showCustom(QImage &&image) { @@ -1122,6 +1156,40 @@ rpl::producer<> UserpicButton::resetPersonalRequests() const { return _resetPersonalRequests.events(); } +void UserpicButton::showUploadProgress() { + if (_uploadOverlay && _uploadOverlay->uploading()) { + return; + } + Expects(_peer != nullptr); + + _uploadOverlay = std::make_unique( + this, + [=] { update(); }); + _uploadOverlay->start(); + + _peer->session().api().peerPhoto().subscribeToUpload( + _peer, + _uploadLifetime, + { + .progress = [=](float64 value) { + _uploadOverlay->setProgress(value); + }, + .done = [=] { + _uploadOverlay->stop([=] { + _uploadLifetime.destroy(); + _uploadOverlay = nullptr; + }); + }, + .failed = [=] { + _uploadOverlay->fail([=] { + _uploadLifetime.destroy(); + _uploadOverlay = nullptr; + showSource(Source::PeerPhoto); + }); + }, + }); +} + void UserpicButton::fillShape(QPainter &p, QBrush brush) const { PainterHighQualityEnabler hq(p); p.setPen(Qt::NoPen); diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.h b/Telegram/SourceFiles/ui/controls/userpic_button.h index 39efca6c3c..a23b65c96e 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.h +++ b/Telegram/SourceFiles/ui/controls/userpic_button.h @@ -40,6 +40,8 @@ class ItemBase; namespace Ui { +class UploadProgressOverlay; + class PopupMenu; class UserpicButton final : public RippleButton { @@ -111,6 +113,8 @@ public: void showSource(Source source); void showCustomOnChosen(); + void showUploadProgress(); + [[nodiscard]] PopupMenu *showChangePhotoMenu(); void overrideHasPersonalPhoto(bool has); @@ -202,6 +206,9 @@ private: rpl::event_stream<> _resetPersonalRequests; rpl::lifetime _sourceLifetime; + std::unique_ptr _uploadOverlay; + rpl::lifetime _uploadLifetime; + }; [[nodiscard]] not_null CreateUploadSubButton( diff --git a/Telegram/SourceFiles/ui/effects/upload_progress_overlay.cpp b/Telegram/SourceFiles/ui/effects/upload_progress_overlay.cpp new file mode 100644 index 0000000000..ecdcb1387c --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/upload_progress_overlay.cpp @@ -0,0 +1,198 @@ +/* +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 "ui/effects/upload_progress_overlay.h" + +#include "base/call_delayed.h" +#include "ui/effects/animation_value.h" +#include "ui/painter.h" +#include "styles/style_basic.h" + +namespace Ui { + +UploadProgressOverlay::UploadProgressOverlay( + not_null parent, + Fn update) +: _parent(parent) +, _update(std::move(update)) +, _radial([=](crl::time now) { radialAnimationCallback(now); }) { +} + +void UploadProgressOverlay::start() { + _uploading = true; + _stopping = false; + _startedAt = crl::now(); + _progress = 0.; + _hiding.stop(); + _hidingDone = nullptr; + _radial.start(_progress); + _update(); +} + +void UploadProgressOverlay::stop(Fn done) { + if (!_uploading) { + if (done) { + done(); + } + return; + } + _hidingDone = std::move(done); + _stopping = true; + _progress = 1.; + _update(); + + const auto catchUp = st::radialDuration * 2; + base::call_delayed(catchUp, _parent, [=] { + if (!_stopping) { + return; + } + proceedWithStop(); + }); +} + +void UploadProgressOverlay::fail(Fn done) { + if (!_uploading) { + if (done) { + done(); + } + return; + } + _hidingDone = std::move(done); + _stopping = true; + _progress = 0.; + _update(); + + const auto catchUp = st::radialDuration * 2; + base::call_delayed(catchUp, _parent, [=] { + if (!_stopping) { + return; + } + proceedWithStop(); + }); +} + +void UploadProgressOverlay::proceedWithStop() { + _stopping = false; + _uploading = false; + + const auto elapsed = crl::now() - _startedAt; + const auto minDuration = st::radialDuration * 3; + const auto delay = std::max(minDuration - elapsed, crl::time(0)); + base::call_delayed(delay, _parent, [=] { + if (_uploading) { + return; + } + _hiding.start( + _update, + 1., + 0., + st::radialDuration); + _hiding.setFinishedCallback([=] { + _radial.stop(); + const auto callback = base::take(_hidingDone); + _update(); + if (callback) { + callback(); + } + }); + }); +} + +void UploadProgressOverlay::finish(Fn done) { + _uploading = false; + _update(); + if (done) { + done(); + } +} + +void UploadProgressOverlay::setProgress(float64 progress) { + _progress = progress; + _update(); +} + +void UploadProgressOverlay::setOver(bool over) { + _over = over; + if (!_uploading) { + return; + } + _cancelShown.start( + _update, + over ? 0. : 1., + over ? 1. : 0., + st::universalDuration); +} + +bool UploadProgressOverlay::shown() const { + return _uploading + || _radial.animating() + || _hiding.animating() + || _hidingDone; +} + +bool UploadProgressOverlay::uploading() const { + return _uploading && !_stopping; +} + +void UploadProgressOverlay::paint( + QPainter &p, + QRect rect, + const PaintArgs &args) { + const auto hideOpacity = _hiding.animating() + ? _hiding.value(0.) + : (_uploading || _hidingDone) ? 1. : 0.; + if (hideOpacity <= 0.) { + return; + } + + auto o = p.opacity(); + if (hideOpacity < 1.) { + p.setOpacity(o * hideOpacity); + } + + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(args.overlayFg); + if (args.roundRadius > 0.) { + p.drawRoundedRect(rect, args.roundRadius, args.roundRadius); + } else { + p.drawEllipse(rect); + } + + if (_uploading) { + const auto cancelOpacity = _cancelShown.value( + _over ? 1. : 0.); + if (cancelOpacity > 0. && args.cancelIcon) { + p.setOpacity(o * hideOpacity * cancelOpacity); + args.cancelIcon->paintInCenter(p, rect); + } + } + + p.setOpacity(o * hideOpacity); + const auto line = float64(args.lineWidth); + const auto margin = float64(args.margin); + const auto arc = QRectF(rect) - QMarginsF( + margin, + margin, + margin, + margin); + _radial.draw(p, arc, line, args.progressFg); + + p.setOpacity(o); +} + +void UploadProgressOverlay::radialAnimationCallback(crl::time now) { + const auto updated = _radial.update( + _progress, + !_uploading, + now); + if (!anim::Disabled() || updated || _radial.animating()) { + _update(); + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/upload_progress_overlay.h b/Telegram/SourceFiles/ui/effects/upload_progress_overlay.h new file mode 100644 index 0000000000..7608b449b6 --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/upload_progress_overlay.h @@ -0,0 +1,60 @@ +/* +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 +*/ +#pragma once + +#include "ui/effects/radial_animation.h" +#include "ui/effects/animations.h" + +namespace Ui { + +class UploadProgressOverlay final { +public: + struct PaintArgs { + int lineWidth = 3; + int margin = 8; + style::color progressFg; + style::color overlayFg; + const style::icon *cancelIcon = nullptr; + float64 roundRadius = 0.; + }; + + UploadProgressOverlay( + not_null parent, + Fn update); + + void start(); + void stop(Fn done = nullptr); + void fail(Fn done = nullptr); + void setProgress(float64 progress); + void setOver(bool over); + + [[nodiscard]] bool shown() const; + [[nodiscard]] bool uploading() const; + + void paint(QPainter &p, QRect rect, const PaintArgs &args); + +private: + void radialAnimationCallback(crl::time now); + void finish(Fn done); + void proceedWithStop(); + + const not_null _parent; + const Fn _update; + RadialAnimation _radial; + Animations::Simple _cancelShown; + Animations::Simple _hiding; + Fn _hidingDone; + bool _uploading = false; + bool _stopping = false; + bool _over = false; + float64 _progress = 0.; + crl::time _startedAt = 0; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 9d4b3bd2d8..51828b9eb2 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -494,6 +494,8 @@ PRIVATE ui/effects/snowflakes.h ui/effects/toggle_arrow.cpp ui/effects/toggle_arrow.h + ui/effects/upload_progress_overlay.cpp + ui/effects/upload_progress_overlay.h ui/effects/ttl_icon.cpp ui/effects/ttl_icon.h ui/search_field_controller.cpp