From 1a664edd394ae97f20773f05380bfc8df4b67647 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 22 Feb 2022 16:58:55 +0300 Subject: [PATCH] Initial implementation of download progress bar. --- .../data/data_download_manager.cpp | 200 +++++++++++++---- .../SourceFiles/data/data_download_manager.h | 46 +++- Telegram/SourceFiles/dialogs/dialogs.style | 19 ++ .../SourceFiles/dialogs/dialogs_widget.cpp | 48 +++- Telegram/SourceFiles/dialogs/dialogs_widget.h | 3 + .../SourceFiles/ui/controls/download_bar.cpp | 209 ++++++++++++++++++ .../SourceFiles/ui/controls/download_bar.h | 66 ++++++ Telegram/cmake/td_ui.cmake | 2 + Telegram/lib_ui | 2 +- 9 files changed, 539 insertions(+), 56 deletions(-) create mode 100644 Telegram/SourceFiles/ui/controls/download_bar.cpp create mode 100644 Telegram/SourceFiles/ui/controls/download_bar.h diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index 6dcec29162..84161be39d 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -17,10 +17,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_account.h" #include "history/history.h" #include "history/history_item.h" +#include "core/application.h" +#include "ui/controls/download_bar.h" namespace Data { namespace { +constexpr auto kClearLoadingTimeout = 5 * crl::time(1000); + constexpr auto ByItem = [](const auto &entry) { if constexpr (std::is_same_v) { return entry.object.item; @@ -41,7 +45,9 @@ constexpr auto ByItem = [](const auto &entry) { } // namespace -DownloadManager::DownloadManager() = default; +DownloadManager::DownloadManager() +: _clearLoadingTimer([=] { clearLoading(); }) { +} DownloadManager::~DownloadManager() = default; @@ -50,14 +56,14 @@ void DownloadManager::trackSession(not_null session) { session->data().itemRepaintRequest( ) | rpl::filter([=](not_null item) { - return _downloading.contains(item); + return _loading.contains(item); }) | rpl::start_with_next([=](not_null item) { check(item); }, data.lifetime); session->data().itemLayoutChanged( ) | rpl::filter([=](not_null item) { - return _downloading.contains(item); + return _loading.contains(item); }) | rpl::start_with_next([=](not_null item) { check(item); }, data.lifetime); @@ -104,8 +110,13 @@ void DownloadManager::addLoading(DownloadObject object) { : object.photo->imageByteSize(PhotoSize::Large); data.downloading.push_back({ .object = object, .total = size }); - _downloading.emplace(item); - _loadingTotal += size; + _loading.emplace(item); + _loadingProgress = DownloadProgress{ + .ready = _loadingProgress.current().ready, + .total = _loadingProgress.current().total + size, + }; + _loadingListChanges.fire({}); + _clearLoadingTimer.cancel(); check(item); } @@ -120,12 +131,7 @@ void DownloadManager::check(not_null item) { const auto photo = media ? media->photo() : nullptr; const auto document = media ? media->document() : nullptr; if (entry.object.photo != photo || entry.object.document != document) { - if (const auto document = entry.object.document) { - document->cancel(); - } else if (const auto photo = entry.object.photo) { - photo->cancel(); - } - remove(data, i); + cancel(data, i); return; } // Load with progress only documents for now. @@ -133,19 +139,23 @@ void DownloadManager::check(not_null item) { const auto path = document->filepath(true); if (!path.isEmpty()) { - addLoaded(entry.object, path, entry.started); + if (_loading.contains(item)) { + addLoaded(entry.object, path, entry.started); + } } else if (!document->loading()) { remove(data, i); } else { - const auto total = document->size; - const auto ready = document->loadOffset(); - if (total == entry.total && ready == entry.ready) { + const auto totalChange = document->size - entry.total; + const auto readyChange = document->loadOffset() - entry.ready; + if (!readyChange && !totalChange) { return; } - _loadingTotal += (total - entry.total); - _loadingReady += (ready - entry.ready); - entry.total = total; - entry.ready = ready; + entry.ready += readyChange; + entry.total += totalChange; + _loadingProgress = DownloadProgress{ + .ready = _loadingProgress.current().ready + readyChange, + .total = _loadingProgress.current().total + totalChange, + }; } } @@ -170,18 +180,61 @@ void DownloadManager::addLoaded( .peerAccessHash = PeerAccessHash(item->history()->peer), .object = std::make_unique(object), }); - _downloaded.emplace(item); + _loaded.emplace(item); const auto i = ranges::find(data.downloading, item, ByItem); if (i != end(data.downloading)) { auto &entry = *i; - const auto size = entry.total; - remove(data, i); - if (_downloading.empty()) { - Assert(_loadingTotal == 0 && _loadingReady == 0); - _loadedTotal = 0; - } else { - _loadedTotal += size; + const auto j = _loading.find(entry.object.item); + if (j == end(_loading)) { + return; + } + const auto document = entry.object.document; + const auto totalChange = document->size - entry.total; + const auto readyChange = document->size - entry.ready; + entry.ready += readyChange; + entry.total += totalChange; + entry.done = true; + _loading.erase(j); + _loadingDone.emplace(entry.object.item); + _loadingProgress = DownloadProgress{ + .ready = _loadingProgress.current().ready + readyChange, + .total = _loadingProgress.current().total + totalChange, + }; + if (_loading.empty()) { + _clearLoadingTimer.callOnce(kClearLoadingTimeout); + } + } +} + +auto DownloadManager::loadingList() const +-> ranges::any_view { + return ranges::views::all( + _sessions + ) | ranges::views::transform([=](const auto &pair) { + return ranges::views::all(pair.second.downloading); + }) | ranges::views::join; +} + +DownloadProgress DownloadManager::loadingProgress() const { + return _loadingProgress.current(); +} + +rpl::producer<> DownloadManager::loadingListChanges() const { + return _loadingListChanges.events(); +} + +auto DownloadManager::loadingProgressValue() const +-> rpl::producer { + return _loadingProgress.value(); +} + +void DownloadManager::clearLoading() { + Expects(_loading.empty()); + + for (auto &[session, data] : _sessions) { + while (!data.downloading.empty()) { + remove(data, data.downloading.end() - 1); } } } @@ -189,16 +242,34 @@ void DownloadManager::addLoaded( void DownloadManager::remove( SessionData &data, std::vector::iterator i) { - _loadingTotal -= i->total; - _loadingReady -= i->ready; - _downloading.remove(i->object.item); + const auto now = DownloadProgress{ + .ready = _loadingProgress.current().ready - i->ready, + .total = _loadingProgress.current().total - i->total, + }; + _loading.remove(i->object.item); + _loadingDone.remove(i->object.item); data.downloading.erase(i); + _loadingListChanges.fire({}); + _loadingProgress = now; + if (_loading.empty() && !_loadingDone.empty()) { + _clearLoadingTimer.callOnce(kClearLoadingTimeout); + } +} + +void DownloadManager::cancel( + SessionData &data, + std::vector::iterator i) { + const auto object = i->object; + remove(data, i); + if (const auto document = object.document) { + document->cancel(); + } else if (const auto photo = object.photo) { + photo->cancel(); + } } void DownloadManager::changed(not_null item) { - if (_downloading.contains(item)) { - check(item); - } else if (_downloaded.contains(item)) { + if (_loaded.contains(item)) { auto &data = sessionData(item); const auto i = ranges::find(data.downloaded, item, ByItem); Assert(i != end(data.downloaded)); @@ -209,13 +280,24 @@ void DownloadManager::changed(not_null item) { if (i->object->photo != photo || i->object->document != document) { *i->object = DownloadObject(); - _downloaded.remove(item); + _loaded.remove(item); } } + if (_loading.contains(item) || _loadingDone.contains(item)) { + check(item); + } } void DownloadManager::removed(not_null item) { - if (_downloading.contains(item)) { + if (_loaded.contains(item)) { + auto &data = sessionData(item); + const auto i = ranges::find(data.downloaded, item, ByItem); + Assert(i != end(data.downloaded)); + *i->object = DownloadObject(); + + _loaded.remove(item); + } + if (_loading.contains(item) || _loadingDone.contains(item)) { auto &data = sessionData(item); const auto i = ranges::find(data.downloading, item, ByItem); Assert(i != end(data.downloading)); @@ -224,19 +306,7 @@ void DownloadManager::removed(not_null item) { // We don't want to download files without messages. // For example, there is no way to refresh a file reference for them. //entry.object.item = nullptr; - if (const auto document = entry.object.document) { - document->cancel(); - } else if (const auto photo = entry.object.photo) { - photo->cancel(); - } - remove(data, i); - } else if (_downloaded.contains(item)) { - auto &data = sessionData(item); - const auto i = ranges::find(data.downloaded, item, ByItem); - Assert(i != end(data.downloaded)); - *i->object = DownloadObject(); - - _downloaded.remove(item); + cancel(data, i); } } @@ -259,7 +329,7 @@ void DownloadManager::untrack(not_null session) { for (const auto &entry : i->second.downloaded) { if (const auto resolved = entry.object.get()) { if (const auto item = resolved->item) { - _downloaded.remove(item); + _loaded.remove(item); } } } @@ -269,4 +339,36 @@ void DownloadManager::untrack(not_null session) { _sessions.erase(i); } +rpl::producer MakeDownloadBarProgress() { + return Core::App().downloadManager().loadingProgressValue( + ) | rpl::map([=](const DownloadProgress &progress) { + return Ui::DownloadBarProgress{ + .ready = progress.ready, + .total = progress.total, + }; + }); +} + +rpl::producer MakeDownloadBarContent() { + auto &manager = Core::App().downloadManager(); + return rpl::single( + rpl::empty_value() + ) | rpl::then( + manager.loadingListChanges() | rpl::to_empty + ) | rpl::map([=, &manager] { + auto result = Ui::DownloadBarContent(); + for (const auto &id : manager.loadingList()) { + if (result.singleName.isEmpty()) { + result.singleName = id.object.document->filename(); + result.singleThumbnail = QImage(); + } + ++result.count; + if (id.done) { + ++result.done; + } + } + return result; + }); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_download_manager.h b/Telegram/SourceFiles/data/data_download_manager.h index 2c6a4cfadf..08fdd76466 100644 --- a/Telegram/SourceFiles/data/data_download_manager.h +++ b/Telegram/SourceFiles/data/data_download_manager.h @@ -7,6 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/timer.h" + +namespace Ui { +struct DownloadBarProgress; +struct DownloadBarContent; +} // namespace Ui + namespace Main { class Session; } // namespace Main @@ -26,6 +33,16 @@ struct DownloadId { DownloadType type = DownloadType::Document; }; +struct DownloadProgress { + int64 ready = 0; + int64 total = 0; +}; +inline constexpr bool operator==( + const DownloadProgress &a, + const DownloadProgress &b) { + return (a.ready == b.ready) && (a.total == b.total); +} + struct DownloadObject { HistoryItem *item = nullptr; DocumentData *document = nullptr; @@ -48,6 +65,7 @@ struct DownloadingId { DownloadDate started = 0; int ready = 0; int total = 0; + bool done = false; }; class DownloadManager final { @@ -63,6 +81,13 @@ public: const QString &path, DownloadDate started); + [[nodiscard]] auto loadingList() const + -> ranges::any_view; + [[nodiscard]] DownloadProgress loadingProgress() const; + [[nodiscard]] rpl::producer<> loadingListChanges() const; + [[nodiscard]] auto loadingProgressValue() const + -> rpl::producer; + private: struct SessionData { std::vector downloaded; @@ -77,19 +102,30 @@ private: void remove( SessionData &data, std::vector::iterator i); + void cancel( + SessionData &data, + std::vector::iterator i); + void clearLoading(); [[nodiscard]] SessionData &sessionData(not_null session); [[nodiscard]] SessionData &sessionData( not_null item); base::flat_map, SessionData> _sessions; - base::flat_set> _downloading; - base::flat_set> _downloaded; + base::flat_set> _loading; + base::flat_set> _loadingDone; + base::flat_set> _loaded; - int64 _loadedTotal = 0; - int64 _loadingReady = 0; - int64 _loadingTotal = 0; + rpl::event_stream<> _loadingListChanges; + rpl::variable _loadingProgress; + + base::Timer _clearLoadingTimer; }; +[[nodiscard]] auto MakeDownloadBarProgress() +-> rpl::producer; + +[[nodiscard]] rpl::producer MakeDownloadBarContent(); + } // namespace Data diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index e02caaaf23..f38bf177b5 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -309,3 +309,22 @@ dialogsUnreadMentionActive: icon{{ "dialogs/dialogs_mention", dialogsUnreadFgAct dialogsUnreadReaction: icon{{ "dialogs/dialogs_reaction", dialogsUnreadFg }}; dialogsUnreadReactionOver: icon{{ "dialogs/dialogs_reaction", dialogsUnreadFgOver }}; dialogsUnreadReactionActive: icon{{ "dialogs/dialogs_reaction", dialogsUnreadFgActive }}; + +downloadBarHeight: 46px; +downloadArrow: icon{{ "fast_to_original", menuIconFg }}; +downloadArrowOver: icon{{ "fast_to_original", menuIconFgOver }}; +downloadArrowRight: 10px; +downloadTitleLeft: 57px; +downloadTitleTop: 4px; +downloadInfoStyle: TextStyle(defaultTextStyle) { + font: font(12px); + linkFont: font(12px); + linkFontOver: font(12px underline); +} +downloadInfoLeft: 57px; +downloadInfoTop: 23px; +downloadLoadingLeft: 15px; +downloadLoadingSize: 24px; +downloadLoadingLine: 2px; +downloadLoadedSize: 30px; +downloadIconDocument: icon {{ "history_file_document", windowFgActive }}; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 79f6cb38e2..22f156e5a1 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/input_fields.h" #include "ui/wrap/fade_wrap.h" #include "ui/effects/radial_animation.h" +#include "ui/controls/download_bar.h" #include "ui/ui_utility.h" #include "lang/lang_keys.h" #include "mainwindow.h" @@ -46,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_histories.h" #include "data/data_changes.h" +#include "data/data_download_manager.h" #include "facades.h" #include "styles/style_dialogs.h" #include "styles/style_chat.h" @@ -361,6 +363,8 @@ Widget::Widget( ) | rpl::start_with_next([=](Data::Folder *folder) { changeOpenedFolder(folder, anim::type::normal); }, lifetime()); + + setupDownloadBar(); } void Widget::setGeometryWithTopMoved( @@ -399,6 +403,47 @@ void Widget::setupScrollUpButton() { updateScrollUpVisibility(); } +void Widget::setupDownloadBar() { + Data::MakeDownloadBarContent( + ) | rpl::start_with_next([=](Ui::DownloadBarContent &&content) { + const auto create = (content.count && !_downloadBar); + if (create) { + _downloadBar = std::make_unique( + this, + Data::MakeDownloadBarProgress()); + } + if (_downloadBar) { + _downloadBar->show(std::move(content)); + } + if (create) { + _downloadBar->heightValue( + ) | rpl::start_with_next([=] { + updateControlsGeometry(); + }, _downloadBar->lifetime()); + + _downloadBar->shownValue( + ) | rpl::filter( + !rpl::mappers::_1 + ) | rpl::start_with_next([=] { + _downloadBar = nullptr; + }, _downloadBar->lifetime()); + + _downloadBar->clicks( + ) | rpl::start_with_next([=] { + auto &&list = Core::App().downloadManager().loadingList(); + for (const auto &id : list) { + controller()->showPeerHistoryAtItem(id.object.item); + break; + } + }, _downloadBar->lifetime()); + + if (_connecting) { + _connecting->raise(); + } + } + }, lifetime()); +} + void Widget::updateScrollUpVisibility() { if (_scrollToAnimation.animating()) { return; @@ -1632,7 +1677,7 @@ void Widget::updateControlsGeometry() { auto scrollTop = filterAreaTop + filterAreaHeight; auto newScrollTop = _scroll->scrollTop() + _topDelta; auto scrollHeight = height() - scrollTop; - const auto putBottomButton = [&](object_ptr &button) { + const auto putBottomButton = [&](auto &button) { if (button && !button->isHidden()) { const auto buttonHeight = button->height(); scrollHeight -= buttonHeight; @@ -1644,6 +1689,7 @@ void Widget::updateControlsGeometry() { } }; putBottomButton(_updateTelegram); + putBottomButton(_downloadBar); putBottomButton(_loadMoreChats); auto wasScrollHeight = _scroll->height(); _scroll->setGeometry(0, scrollTop, width(), scrollHeight); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index dfd9f435e7..429a849ff5 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -36,6 +36,7 @@ class DropdownMenu; class FlatButton; class FlatInput; class CrossButton; +class DownloadBar; template class FadeWrapScaled; } // namespace Ui @@ -148,6 +149,7 @@ private: void setupSupportMode(); void setupConnectingWidget(); void setupMainMenuToggle(); + void setupDownloadBar(); bool searchForPeersRequired(const QString &query) const; void setSearchInChat(Key chat, PeerData *from = nullptr); void showCalendar(); @@ -204,6 +206,7 @@ private: class BottomButton; object_ptr _updateTelegram = { nullptr }; object_ptr _loadMoreChats = { nullptr }; + std::unique_ptr _downloadBar; std::unique_ptr _connecting; Ui::Animations::Simple _scrollToAnimation; diff --git a/Telegram/SourceFiles/ui/controls/download_bar.cpp b/Telegram/SourceFiles/ui/controls/download_bar.cpp new file mode 100644 index 0000000000..e9cfcb0806 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/download_bar.cpp @@ -0,0 +1,209 @@ +/* +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/controls/download_bar.h" + +#include "ui/widgets/buttons.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "lang/lang_keys.h" +#include "styles/style_dialogs.h" + +namespace Ui { +namespace { + +constexpr auto kFullArcLength = 360 * 16; + +} // namespace + +DownloadBar::DownloadBar( + not_null parent, + rpl::producer progress) +: _button( + parent, + object_ptr(parent, st::dialogsMenuToggle.ripple)) +, _shadow(parent) +, _progress(std::move(progress)) +, _radial([=](crl::time now) { radialAnimationCallback(now); }) { + _button.hide(anim::type::instant); + _shadow.showOn(_button.shownValue()); + _button.setDirectionUp(false); + _button.entity()->resize(0, st::downloadBarHeight); + _button.entity()->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + auto p = Painter(_button.entity()); + paint(p, clip); + }, lifetime()); + + _progress.value( + ) | rpl::start_with_next([=](const DownloadBarProgress &progress) { + refreshInfo(progress); + }, lifetime()); +} + +DownloadBar::~DownloadBar() = default; + +void DownloadBar::show(DownloadBarContent &&content) { + _button.toggle(content.count > 0, anim::type::normal); + if (!content.count) { + return; + } + if (!_radial.animating()) { + _radial.start(computeProgress()); + } + _content = content; + _title.setText(st::semiboldTextStyle, + (content.count > 1 + ? tr::lng_profile_files(tr::now, lt_count, content.count) + : content.singleName)); + refreshInfo(_progress.current()); +} + +void DownloadBar::refreshInfo(const DownloadBarProgress &progress) { + _info.setMarkedText(st::downloadInfoStyle, + (progress.ready < progress.total + ? Text::WithEntities( + FormatDownloadText(progress.ready, progress.total)) + : (_content.count > 1) + ? Text::Link(u"View downloads"_q) + : Text::Link(u"View in chat"_q))); + _button.entity()->update(); +} + +bool DownloadBar::isHidden() const { + return _button.isHidden(); +} + +int DownloadBar::height() const { + return _button.height(); +} + +rpl::producer DownloadBar::heightValue() const { + return _button.heightValue(); +} + +rpl::producer DownloadBar::shownValue() const { + return _button.shownValue(); +} + +void DownloadBar::setGeometry(int left, int top, int width, int height) { + _button.resizeToWidth(width); + _button.moveToLeft(left, top); + _shadow.setGeometry(left, top - st::lineWidth, width, st::lineWidth); +} + +rpl::producer<> DownloadBar::clicks() const { + return _button.entity()->clicks() | rpl::to_empty; +} + +rpl::lifetime &DownloadBar::lifetime() { + return _button.lifetime(); +} + +void DownloadBar::paint(Painter &p, QRect clip) { + const auto button = _button.entity(); + const auto outerw = button->width(); + const auto over = button->isOver() || button->isDown(); + const auto &icon = over ? st::downloadArrowOver : st::downloadArrow; + p.fillRect(clip, st::windowBg); + button->paintRipple(p, 0, 0); + + const auto size = st::downloadLoadingSize; + const auto added = 3 * st::downloadLoadingLine; + const auto skipx = st::downloadLoadingLeft; + const auto skipy = (button->height() - size) / 2; + const auto full = QRect( + skipx - added, + skipy - added, + size + added * 2, + size + added * 2); + if (full.intersects(clip)) { + const auto loading = _radial.computeState(); + { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::windowBgActive); + p.drawEllipse(full.marginsRemoved({ added, added, added, added })); + + if (loading.shown > 0) { + p.setOpacity(loading.shown); + auto pen = st::windowBgActive->p; + pen.setWidth(st::downloadLoadingLine); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + const auto m = added / 2.; + auto rect = QRectF(full).marginsRemoved({ m, m, m, m }); + if (loading.arcLength < kFullArcLength) { + p.drawArc(rect, loading.arcFrom, loading.arcLength); + } else { + p.drawEllipse(rect); + } + p.setOpacity(1.); + } + } + p.save(); + p.translate(full.center()); + p.scale(0.6, 0.6); + p.translate(-full.center()); + st::downloadIconDocument.paintInCenter(p, full); + p.restore(); + } + + const auto minleft = std::min( + st::downloadTitleLeft, + st::downloadInfoLeft); + const auto maxwidth = outerw - minleft; + if (!clip.intersects({ minleft, 0, maxwidth, st::downloadBarHeight })) { + return; + } + const auto right = st::downloadArrowRight + icon.width(); + const auto available = button->width() - st::downloadTitleLeft - right; + p.setPen(st::windowBoldFg); + _title.drawLeftElided( + p, + st::downloadTitleLeft, + st::downloadTitleTop, + available, + outerw); + + p.setPen(st::windowSubTextFg); + p.setTextPalette(st::defaultTextPalette); + _info.drawLeftElided( + p, + st::downloadInfoLeft, + st::downloadInfoTop, + available, + outerw); + + const auto iconTop = (st::downloadBarHeight - icon.height()) / 2; + icon.paint(p, outerw - right, iconTop, outerw); +} + +float64 DownloadBar::computeProgress() const { + const auto now = _progress.current(); + return now.total ? (now.ready / float64(now.total)) : 0.; +} + +void DownloadBar::radialAnimationCallback(crl::time now) { + const auto finished = (_content.done == _content.count); + const auto updated = _radial.update(computeProgress(), finished, now); + if (!anim::Disabled() || updated) { + const auto button = _button.entity(); + const auto size = st::downloadLoadingSize; + const auto added = 3 * st::downloadLoadingLine; + const auto skipx = st::downloadLoadingLeft; + const auto skipy = (button->height() - size) / 2; + const auto full = QRect( + skipx - added, + skipy - added, + size + added * 2, + size + added * 2); + button->update(full); + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/download_bar.h b/Telegram/SourceFiles/ui/controls/download_bar.h new file mode 100644 index 0000000000..d44a96548b --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/download_bar.h @@ -0,0 +1,66 @@ +/* +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/wrap/slide_wrap.h" +#include "ui/effects/radial_animation.h" +#include "ui/widgets/shadow.h" +#include "ui/text/text.h" + +namespace Ui { + +class RippleButton; + +struct DownloadBarProgress { + int64 ready = 0; + int64 total = 0; +}; + +struct DownloadBarContent { + QString singleName; + QImage singleThumbnail; + int count = 0; + int done = 0; +}; + +class DownloadBar final { +public: + DownloadBar( + not_null parent, + rpl::producer progress); + ~DownloadBar(); + + void show(DownloadBarContent &&content); + + [[nodiscard]] bool isHidden() const; + [[nodiscard]] int height() const; + [[nodiscard]] rpl::producer heightValue() const; + [[nodiscard]] rpl::producer shownValue() const; + void setGeometry(int left, int top, int width, int height); + + [[nodiscard]] rpl::producer<> clicks() const; + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + void paint(Painter &p, QRect clip); + void refreshInfo(const DownloadBarProgress &progress); + void radialAnimationCallback(crl::time now); + [[nodiscard]] float64 computeProgress() const; + + SlideWrap _button; + PlainShadow _shadow; + DownloadBarContent _content; + rpl::variable _progress; + Text::String _title; + Text::String _info; + RadialAnimation _radial; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index ceaea64664..3df88554bf 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -192,6 +192,8 @@ PRIVATE ui/controls/call_mute_button.h ui/controls/delete_message_context_action.cpp ui/controls/delete_message_context_action.h + ui/controls/download_bar.cpp + ui/controls/download_bar.h ui/controls/emoji_button.cpp ui/controls/emoji_button.h ui/controls/invite_link_buttons.cpp diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 43c61172d8..6316443b27 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 43c61172d8d2ef08c2c190d6b3425823727ea9c9 +Subproject commit 6316443b27cffbf0205e07edc0616d370044893c