mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-06 23:24:01 +02:00
Allow Info::Media::ListWidget use different data providers.
This commit is contained in:
parent
eefb9823e1
commit
91c46dbc85
36 changed files with 2491 additions and 1237 deletions
|
@ -712,13 +712,25 @@ PRIVATE
|
|||
info/common_groups/info_common_groups_inner_widget.h
|
||||
info/common_groups/info_common_groups_widget.cpp
|
||||
info/common_groups/info_common_groups_widget.h
|
||||
info/downloads/info_downloads_inner_widget.cpp
|
||||
info/downloads/info_downloads_inner_widget.h
|
||||
info/downloads/info_downloads_provider.cpp
|
||||
info/downloads/info_downloads_provider.h
|
||||
info/downloads/info_downloads_widget.cpp
|
||||
info/downloads/info_downloads_widget.h
|
||||
info/media/info_media_buttons.h
|
||||
info/media/info_media_common.cpp
|
||||
info/media/info_media_common.h
|
||||
info/media/info_media_empty_widget.cpp
|
||||
info/media/info_media_empty_widget.h
|
||||
info/media/info_media_inner_widget.cpp
|
||||
info/media/info_media_inner_widget.h
|
||||
info/media/info_media_list_section.cpp
|
||||
info/media/info_media_list_section.h
|
||||
info/media/info_media_list_widget.cpp
|
||||
info/media/info_media_list_widget.h
|
||||
info/media/info_media_provider.cpp
|
||||
info/media/info_media_provider.h
|
||||
info/media/info_media_widget.cpp
|
||||
info/media/info_media_widget.h
|
||||
info/members/info_members_widget.cpp
|
||||
|
|
|
@ -24,31 +24,31 @@ public:
|
|||
, _skippedAfter(skippedAfter) {
|
||||
}
|
||||
|
||||
std::optional<int> fullCount() const {
|
||||
[[nodiscard]] std::optional<int> fullCount() const {
|
||||
return _fullCount;
|
||||
}
|
||||
std::optional<int> skippedBefore() const {
|
||||
[[nodiscard]] std::optional<int> skippedBefore() const {
|
||||
return _skippedBefore;
|
||||
}
|
||||
std::optional<int> skippedAfter() const {
|
||||
[[nodiscard]] std::optional<int> skippedAfter() const {
|
||||
return _skippedAfter;
|
||||
}
|
||||
std::optional<int> indexOf(Id id) const {
|
||||
[[nodiscard]] std::optional<int> indexOf(Id id) const {
|
||||
const auto it = ranges::find(_ids, id);
|
||||
if (it != _ids.end()) {
|
||||
return (it - _ids.begin());
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
int size() const {
|
||||
[[nodiscard]] int size() const {
|
||||
return _ids.size();
|
||||
}
|
||||
Id operator[](int index) const {
|
||||
[[nodiscard]] Id operator[](int index) const {
|
||||
Expects(index >= 0 && index < size());
|
||||
|
||||
return *(_ids.begin() + index);
|
||||
}
|
||||
std::optional<int> distance(Id a, Id b) const {
|
||||
[[nodiscard]] std::optional<int> distance(Id a, Id b) const {
|
||||
if (const auto i = indexOf(a)) {
|
||||
if (const auto j = indexOf(b)) {
|
||||
return *j - *i;
|
||||
|
@ -56,7 +56,8 @@ public:
|
|||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<Id> nearest(Id id) const {
|
||||
[[nodiscard]] std::optional<Id> nearest(Id id) const {
|
||||
static_assert(std::is_same_v<IdsContainer, base::flat_set<MsgId>>);
|
||||
if (const auto it = ranges::lower_bound(_ids, id); it != _ids.end()) {
|
||||
return *it;
|
||||
} else if (_ids.empty()) {
|
||||
|
|
|
@ -181,6 +181,60 @@ struct FullMsgId {
|
|||
|
||||
Q_DECLARE_METATYPE(FullMsgId);
|
||||
|
||||
struct GlobalMsgId {
|
||||
FullMsgId itemId;
|
||||
uint64 sessionUniqueId = 0;
|
||||
|
||||
constexpr explicit operator bool() const noexcept {
|
||||
return itemId && sessionUniqueId;
|
||||
}
|
||||
constexpr bool operator!() const noexcept {
|
||||
return !itemId || !sessionUniqueId;
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] inline constexpr bool operator<(
|
||||
const GlobalMsgId &a,
|
||||
const GlobalMsgId &b) noexcept {
|
||||
if (a.itemId < b.itemId) {
|
||||
return true;
|
||||
} else if (a.itemId > b.itemId) {
|
||||
return false;
|
||||
}
|
||||
return a.sessionUniqueId < b.sessionUniqueId;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr bool operator>(
|
||||
const GlobalMsgId &a,
|
||||
const GlobalMsgId &b) noexcept {
|
||||
return b < a;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr bool operator<=(
|
||||
const GlobalMsgId &a,
|
||||
const GlobalMsgId &b) noexcept {
|
||||
return !(b < a);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr bool operator>=(
|
||||
const GlobalMsgId &a,
|
||||
const GlobalMsgId &b) noexcept {
|
||||
return !(a < b);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr bool operator==(
|
||||
const GlobalMsgId &a,
|
||||
const GlobalMsgId &b) noexcept {
|
||||
return (a.itemId == b.itemId)
|
||||
&& (a.sessionUniqueId == b.sessionUniqueId);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline constexpr bool operator!=(
|
||||
const GlobalMsgId &a,
|
||||
const GlobalMsgId &b) noexcept {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
namespace std {
|
||||
|
||||
template <>
|
||||
|
|
|
@ -28,22 +28,22 @@ SparseIdsMergedSlice::SparseIdsMergedSlice(
|
|||
|
||||
SparseIdsMergedSlice::SparseIdsMergedSlice(
|
||||
Key key,
|
||||
SparseUnsortedIdsSlice scheduled)
|
||||
SparseUnsortedIdsSlice unsorted)
|
||||
: _key(key)
|
||||
, _scheduled(std::move(scheduled)) {
|
||||
, _unsorted(std::move(unsorted)) {
|
||||
}
|
||||
|
||||
std::optional<int> SparseIdsMergedSlice::fullCount() const {
|
||||
return _scheduled
|
||||
? _scheduled->fullCount()
|
||||
return _unsorted
|
||||
? _unsorted->fullCount()
|
||||
: Add(
|
||||
_part.fullCount(),
|
||||
_migrated ? _migrated->fullCount() : 0);
|
||||
}
|
||||
|
||||
std::optional<int> SparseIdsMergedSlice::skippedBefore() const {
|
||||
return _scheduled
|
||||
? _scheduled->skippedBefore()
|
||||
return _unsorted
|
||||
? _unsorted->skippedBefore()
|
||||
: Add(
|
||||
isolatedInMigrated() ? 0 : _part.skippedBefore(),
|
||||
_migrated
|
||||
|
@ -55,8 +55,8 @@ std::optional<int> SparseIdsMergedSlice::skippedBefore() const {
|
|||
}
|
||||
|
||||
std::optional<int> SparseIdsMergedSlice::skippedAfter() const {
|
||||
return _scheduled
|
||||
? _scheduled->skippedAfter()
|
||||
return _unsorted
|
||||
? _unsorted->skippedAfter()
|
||||
: Add(
|
||||
isolatedInMigrated() ? _part.fullCount() : _part.skippedAfter(),
|
||||
isolatedInPart() ? 0 : _migrated->skippedAfter()
|
||||
|
@ -65,8 +65,8 @@ std::optional<int> SparseIdsMergedSlice::skippedAfter() const {
|
|||
|
||||
std::optional<int> SparseIdsMergedSlice::indexOf(
|
||||
FullMsgId fullId) const {
|
||||
return _scheduled
|
||||
? _scheduled->indexOf(fullId.msg)
|
||||
return _unsorted
|
||||
? _unsorted->indexOf(fullId.msg)
|
||||
: isFromPart(fullId)
|
||||
? (_part.indexOf(fullId.msg) | func::add(migratedSize()))
|
||||
: isolatedInPart()
|
||||
|
@ -77,8 +77,8 @@ std::optional<int> SparseIdsMergedSlice::indexOf(
|
|||
}
|
||||
|
||||
int SparseIdsMergedSlice::size() const {
|
||||
return _scheduled
|
||||
? _scheduled->size()
|
||||
return _unsorted
|
||||
? _unsorted->size()
|
||||
: (isolatedInPart() ? 0 : migratedSize())
|
||||
+ (isolatedInMigrated() ? 0 : _part.size());
|
||||
}
|
||||
|
@ -86,8 +86,8 @@ int SparseIdsMergedSlice::size() const {
|
|||
FullMsgId SparseIdsMergedSlice::operator[](int index) const {
|
||||
Expects(index >= 0 && index < size());
|
||||
|
||||
if (_scheduled) {
|
||||
return ComputeId(_key.peerId, (*_scheduled)[index]);
|
||||
if (_unsorted) {
|
||||
return ComputeId(_key.peerId, (*_unsorted)[index]);
|
||||
}
|
||||
|
||||
if (const auto size = migratedSize()) {
|
||||
|
@ -112,10 +112,13 @@ std::optional<int> SparseIdsMergedSlice::distance(
|
|||
|
||||
auto SparseIdsMergedSlice::nearest(
|
||||
UniversalMsgId id) const -> std::optional<FullMsgId> {
|
||||
if (_scheduled) {
|
||||
if (const auto nearestId = _scheduled->nearest(id)) {
|
||||
return ComputeId(_key.peerId, *nearestId);
|
||||
if (_unsorted) {
|
||||
if (_unsorted->indexOf(id).has_value()) {
|
||||
return ComputeId(_key.peerId, id);
|
||||
} else if (const auto count = _unsorted->size()) {
|
||||
return ComputeId(_key.peerId, (*_unsorted)[count / 2]);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto convertFromPartNearest = [&](MsgId result) {
|
||||
return ComputeId(_key.peerId, result);
|
||||
|
|
|
@ -62,7 +62,7 @@ public:
|
|||
std::optional<SparseIdsSlice> migrated);
|
||||
SparseIdsMergedSlice(
|
||||
Key key,
|
||||
SparseUnsortedIdsSlice scheduled);
|
||||
SparseUnsortedIdsSlice unsorted);
|
||||
|
||||
std::optional<int> fullCount() const;
|
||||
std::optional<int> skippedBefore() const;
|
||||
|
@ -139,7 +139,7 @@ private:
|
|||
Key _key;
|
||||
SparseIdsSlice _part;
|
||||
std::optional<SparseIdsSlice> _migrated;
|
||||
std::optional<SparseUnsortedIdsSlice> _scheduled;
|
||||
std::optional<SparseUnsortedIdsSlice> _unsorted;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "storage/file_upload.h"
|
||||
#include "storage/storage_facade.h"
|
||||
#include "storage/storage_shared_media.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_domain.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "media/audio/media_audio.h"
|
||||
|
@ -1017,6 +1019,10 @@ FullMsgId HistoryItem::fullId() const {
|
|||
return FullMsgId(_history->peer->id, id);
|
||||
}
|
||||
|
||||
GlobalMsgId HistoryItem::globalId() const {
|
||||
return { fullId(), _history->session().uniqueId() };
|
||||
}
|
||||
|
||||
Data::MessagePosition HistoryItem::position() const {
|
||||
return { .fullId = fullId(), .date = date() };
|
||||
}
|
||||
|
@ -1301,6 +1307,20 @@ HistoryItem::~HistoryItem() {
|
|||
applyTTL(0);
|
||||
}
|
||||
|
||||
HistoryItem *MessageByGlobalId(GlobalMsgId globalId) {
|
||||
if (!globalId.sessionUniqueId || !globalId.itemId) {
|
||||
return nullptr;
|
||||
}
|
||||
for (const auto &[index, account] : Core::App().domain().accounts()) {
|
||||
if (const auto session = account->maybeSession()) {
|
||||
if (session->uniqueId() == globalId.sessionUniqueId) {
|
||||
return session->data().message(globalId.itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QDateTime ItemDateTime(not_null<const HistoryItem*> item) {
|
||||
return base::unixtime::parse(item->date());
|
||||
}
|
||||
|
|
|
@ -380,6 +380,7 @@ public:
|
|||
[[nodiscard]] bool hasDirectLink() const;
|
||||
|
||||
[[nodiscard]] FullMsgId fullId() const;
|
||||
[[nodiscard]] GlobalMsgId globalId() const;
|
||||
[[nodiscard]] Data::MessagePosition position() const;
|
||||
[[nodiscard]] TimeId date() const;
|
||||
|
||||
|
@ -494,9 +495,14 @@ private:
|
|||
|
||||
};
|
||||
|
||||
QDateTime ItemDateTime(not_null<const HistoryItem*> item);
|
||||
QString ItemDateText(not_null<const HistoryItem*> item, bool isUntilOnline);
|
||||
bool IsItemScheduledUntilOnline(not_null<const HistoryItem*> item);
|
||||
[[nodiscard]] HistoryItem *MessageByGlobalId(GlobalMsgId globalId);
|
||||
|
||||
[[nodiscard]] QDateTime ItemDateTime(not_null<const HistoryItem*> item);
|
||||
[[nodiscard]] QString ItemDateText(
|
||||
not_null<const HistoryItem*> item,
|
||||
bool isUntilOnline);
|
||||
[[nodiscard]] bool IsItemScheduledUntilOnline(
|
||||
not_null<const HistoryItem*> item);
|
||||
|
||||
ClickHandlerPtr goToMessageClickHandler(
|
||||
not_null<PeerData*> peer,
|
||||
|
|
|
@ -282,4 +282,3 @@ void InnerWidget::peerListSetDescription(
|
|||
|
||||
} // namespace CommonGroups
|
||||
} // namespace Info
|
||||
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
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 "info/downloads/info_downloads_inner_widget.h"
|
||||
|
||||
#include "info/downloads/info_downloads_widget.h"
|
||||
#include "info/media/info_media_list_widget.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/search_field_controller.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Info::Downloads {
|
||||
|
||||
class EmptyWidget : public Ui::RpWidget {
|
||||
public:
|
||||
EmptyWidget(QWidget *parent);
|
||||
|
||||
void setFullHeight(rpl::producer<int> fullHeightValue);
|
||||
void setSearchQuery(const QString &query);
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
private:
|
||||
object_ptr<Ui::FlatLabel> _text;
|
||||
int _height = 0;
|
||||
|
||||
};
|
||||
|
||||
EmptyWidget::EmptyWidget(QWidget *parent)
|
||||
: RpWidget(parent)
|
||||
, _text(this, st::infoEmptyLabel) {
|
||||
}
|
||||
|
||||
void EmptyWidget::setFullHeight(rpl::producer<int> fullHeightValue) {
|
||||
std::move(
|
||||
fullHeightValue
|
||||
) | rpl::start_with_next([this](int fullHeight) {
|
||||
// Make icon center be on 1/3 height.
|
||||
auto iconCenter = fullHeight / 3;
|
||||
auto iconHeight = st::infoEmptyFile.height();
|
||||
auto iconTop = iconCenter - iconHeight / 2;
|
||||
_height = iconTop + st::infoEmptyIconTop;
|
||||
resizeToWidth(width());
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void EmptyWidget::setSearchQuery(const QString &query) {
|
||||
_text->setText(query.isEmpty()
|
||||
? tr::lng_media_file_empty(tr::now)
|
||||
: tr::lng_media_file_empty_search(tr::now));
|
||||
resizeToWidth(width());
|
||||
}
|
||||
|
||||
int EmptyWidget::resizeGetHeight(int newWidth) {
|
||||
auto labelTop = _height - st::infoEmptyLabelTop;
|
||||
auto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip;
|
||||
_text->resizeToNaturalWidth(labelWidth);
|
||||
|
||||
auto labelLeft = (newWidth - _text->width()) / 2;
|
||||
_text->moveToLeft(labelLeft, labelTop, newWidth);
|
||||
|
||||
update();
|
||||
return _height;
|
||||
}
|
||||
|
||||
InnerWidget::InnerWidget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller)
|
||||
: RpWidget(parent)
|
||||
, _controller(controller)
|
||||
, _empty(this) {
|
||||
_empty->heightValue(
|
||||
) | rpl::start_with_next(
|
||||
[this] { refreshHeight(); },
|
||||
_empty->lifetime());
|
||||
_list = setupList();
|
||||
}
|
||||
|
||||
void InnerWidget::visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) {
|
||||
setChildVisibleTopBottom(_list, visibleTop, visibleBottom);
|
||||
}
|
||||
|
||||
bool InnerWidget::showInternal(not_null<Memento*> memento) {
|
||||
if (memento->section().type() == Section::Type::Downloads) {
|
||||
restoreState(memento);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
object_ptr<Media::ListWidget> InnerWidget::setupList() {
|
||||
auto result = object_ptr<Media::ListWidget>(
|
||||
this,
|
||||
_controller);
|
||||
result->heightValue(
|
||||
) | rpl::start_with_next(
|
||||
[this] { refreshHeight(); },
|
||||
result->lifetime());
|
||||
using namespace rpl::mappers;
|
||||
result->scrollToRequests(
|
||||
) | rpl::map([widget = result.data()](int to) {
|
||||
return Ui::ScrollToRequest {
|
||||
widget->y() + to,
|
||||
-1
|
||||
};
|
||||
}) | rpl::start_to_stream(
|
||||
_scrollToRequests,
|
||||
result->lifetime());
|
||||
_selectedLists.fire(result->selectedListValue());
|
||||
_listTops.fire(result->topValue());
|
||||
_controller->mediaSourceQueryValue(
|
||||
) | rpl::start_with_next([this](const QString &query) {
|
||||
_empty->setSearchQuery(query);
|
||||
}, result->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
void InnerWidget::saveState(not_null<Memento*> memento) {
|
||||
_list->saveState(&memento->media());
|
||||
}
|
||||
|
||||
void InnerWidget::restoreState(not_null<Memento*> memento) {
|
||||
_list->restoreState(&memento->media());
|
||||
}
|
||||
|
||||
rpl::producer<SelectedItems> InnerWidget::selectedListValue() const {
|
||||
return _selectedLists.events_starting_with(
|
||||
_list->selectedListValue()
|
||||
) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
void InnerWidget::cancelSelection() {
|
||||
_list->cancelSelection();
|
||||
}
|
||||
|
||||
InnerWidget::~InnerWidget() = default;
|
||||
|
||||
int InnerWidget::resizeGetHeight(int newWidth) {
|
||||
_inResize = true;
|
||||
auto guard = gsl::finally([this] { _inResize = false; });
|
||||
|
||||
_list->resizeToWidth(newWidth);
|
||||
_empty->resizeToWidth(newWidth);
|
||||
return recountHeight();
|
||||
}
|
||||
|
||||
void InnerWidget::refreshHeight() {
|
||||
if (_inResize) {
|
||||
return;
|
||||
}
|
||||
resize(width(), recountHeight());
|
||||
}
|
||||
|
||||
int InnerWidget::recountHeight() {
|
||||
auto top = 0;
|
||||
auto listHeight = 0;
|
||||
if (_list) {
|
||||
_list->moveToLeft(0, top);
|
||||
listHeight = _list->heightNoMargins();
|
||||
top += listHeight;
|
||||
}
|
||||
if (listHeight > 0) {
|
||||
_empty->hide();
|
||||
} else {
|
||||
_empty->show();
|
||||
_empty->moveToLeft(0, top);
|
||||
top += _empty->heightNoMargins();
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
void InnerWidget::setScrollHeightValue(rpl::producer<int> value) {
|
||||
using namespace rpl::mappers;
|
||||
_empty->setFullHeight(rpl::combine(
|
||||
std::move(value),
|
||||
_listTops.events_starting_with(
|
||||
_list->topValue()
|
||||
) | rpl::flatten_latest(),
|
||||
_1 - _2));
|
||||
}
|
||||
|
||||
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
|
||||
return _scrollToRequests.events();
|
||||
}
|
||||
|
||||
} // namespace Info::Downloads
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
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/rp_widget.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "base/unique_qptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class SettingsSlider;
|
||||
class VerticalLayout;
|
||||
class SearchFieldController;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Info {
|
||||
|
||||
class Controller;
|
||||
struct SelectedItems;
|
||||
|
||||
namespace Media {
|
||||
class ListWidget;
|
||||
} // namespace Media
|
||||
|
||||
namespace Downloads {
|
||||
|
||||
class Memento;
|
||||
class EmptyWidget;
|
||||
|
||||
class InnerWidget final : public Ui::RpWidget {
|
||||
public:
|
||||
InnerWidget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller);
|
||||
|
||||
bool showInternal(not_null<Memento*> memento);
|
||||
|
||||
void saveState(not_null<Memento*> memento);
|
||||
void restoreState(not_null<Memento*> memento);
|
||||
|
||||
void setScrollHeightValue(rpl::producer<int> value);
|
||||
|
||||
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
|
||||
rpl::producer<SelectedItems> selectedListValue() const;
|
||||
void cancelSelection();
|
||||
|
||||
~InnerWidget();
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) override;
|
||||
|
||||
private:
|
||||
int recountHeight();
|
||||
void refreshHeight();
|
||||
|
||||
object_ptr<Media::ListWidget> setupList();
|
||||
|
||||
const not_null<Controller*> _controller;
|
||||
|
||||
object_ptr<Media::ListWidget> _list = { nullptr };
|
||||
object_ptr<EmptyWidget> _empty;
|
||||
|
||||
bool _inResize = false;
|
||||
|
||||
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
|
||||
rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
|
||||
rpl::event_stream<rpl::producer<int>> _listTops;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Downloads
|
||||
} // namespace Info
|
123
Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp
Normal file
123
Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp
Normal file
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
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 "info/downloads/info_downloads_provider.h"
|
||||
|
||||
#include "info/media/info_media_widget.h"
|
||||
#include "info/media/info_media_list_section.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "storage/storage_shared_media.h"
|
||||
#include "layout/layout_selection.h"
|
||||
|
||||
namespace Info::Downloads {
|
||||
namespace {
|
||||
|
||||
using namespace Media;
|
||||
|
||||
} // namespace
|
||||
|
||||
Provider::Provider(not_null<AbstractController*> controller)
|
||||
: _controller(controller) {
|
||||
//_controller->session().data().itemRemoved(
|
||||
//) | rpl::start_with_next([this](auto item) {
|
||||
// itemRemoved(item);
|
||||
//}, _lifetime);
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
for (auto &layout : _layouts) {
|
||||
layout.second.item->invalidateCache();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
Type Provider::type() {
|
||||
return Type::File;
|
||||
}
|
||||
|
||||
bool Provider::hasSelectRestriction() {
|
||||
return false;
|
||||
}
|
||||
|
||||
rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
|
||||
return rpl::never<bool>();
|
||||
}
|
||||
|
||||
bool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<int> Provider::fullCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Provider::restart() {
|
||||
|
||||
}
|
||||
|
||||
void Provider::checkPreload(
|
||||
QSize viewport,
|
||||
not_null<BaseLayout*> topLayout,
|
||||
not_null<BaseLayout*> bottomLayout,
|
||||
bool preloadTop,
|
||||
bool preloadBottom) {
|
||||
|
||||
}
|
||||
|
||||
void Provider::refreshViewer() {
|
||||
|
||||
}
|
||||
|
||||
rpl::producer<> Provider::refreshed() {
|
||||
return _refreshed.events();
|
||||
}
|
||||
|
||||
std::vector<ListSection> Provider::fillSections(
|
||||
not_null<Overview::Layout::Delegate*> delegate) {
|
||||
return {};
|
||||
}
|
||||
|
||||
rpl::producer<not_null<BaseLayout*>> Provider::layoutRemoved() {
|
||||
return _layoutRemoved.events();
|
||||
}
|
||||
|
||||
BaseLayout *Provider::lookupLayout(const HistoryItem *item) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Provider::isMyItem(not_null<const HistoryItem*> item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Provider::isAfter(
|
||||
not_null<const HistoryItem*> a,
|
||||
not_null<const HistoryItem*> b) {
|
||||
return a < b;
|
||||
}
|
||||
|
||||
void Provider::applyDragSelection(
|
||||
ListSelectedMap &selected,
|
||||
not_null<const HistoryItem*> fromItem,
|
||||
bool skipFrom,
|
||||
not_null<const HistoryItem*> tillItem,
|
||||
bool skipTill) {
|
||||
|
||||
}
|
||||
|
||||
void Provider::saveState(
|
||||
not_null<Media::Memento*> memento,
|
||||
ListScrollTopState scrollState) {
|
||||
|
||||
}
|
||||
|
||||
void Provider::restoreState(
|
||||
not_null<Media::Memento*> memento,
|
||||
Fn<void(ListScrollTopState)> restoreScrollState) {
|
||||
|
||||
}
|
||||
|
||||
} // namespace Info::Downloads
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
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 "info/media/info_media_common.h"
|
||||
|
||||
namespace Info {
|
||||
class AbstractController;
|
||||
} // namespace Info
|
||||
|
||||
namespace Info::Downloads {
|
||||
|
||||
class Provider final : public Media::ListProvider {
|
||||
public:
|
||||
explicit Provider(not_null<AbstractController*> controller);
|
||||
|
||||
Media::Type type() override;
|
||||
bool hasSelectRestriction() override;
|
||||
rpl::producer<bool> hasSelectRestrictionChanges() override;
|
||||
bool isPossiblyMyItem(not_null<const HistoryItem*> item) override;
|
||||
|
||||
std::optional<int> fullCount() override;
|
||||
|
||||
void restart() override;
|
||||
void checkPreload(
|
||||
QSize viewport,
|
||||
not_null<Media::BaseLayout*> topLayout,
|
||||
not_null<Media::BaseLayout*> bottomLayout,
|
||||
bool preloadTop,
|
||||
bool preloadBottom) override;
|
||||
void refreshViewer() override;
|
||||
rpl::producer<> refreshed() override;
|
||||
|
||||
std::vector<Media::ListSection> fillSections(
|
||||
not_null<Overview::Layout::Delegate*> delegate) override;
|
||||
rpl::producer<not_null<Media::BaseLayout*>> layoutRemoved() override;
|
||||
Media::BaseLayout *lookupLayout(const HistoryItem *item) override;
|
||||
bool isMyItem(not_null<const HistoryItem*> item) override;
|
||||
bool isAfter(
|
||||
not_null<const HistoryItem*> a,
|
||||
not_null<const HistoryItem*> b) override;
|
||||
|
||||
void applyDragSelection(
|
||||
Media::ListSelectedMap &selected,
|
||||
not_null<const HistoryItem*> fromItem,
|
||||
bool skipFrom,
|
||||
not_null<const HistoryItem*> tillItem,
|
||||
bool skipTill) override;
|
||||
|
||||
void saveState(
|
||||
not_null<Media::Memento*> memento,
|
||||
Media::ListScrollTopState scrollState) override;
|
||||
void restoreState(
|
||||
not_null<Media::Memento*> memento,
|
||||
Fn<void(Media::ListScrollTopState)> restoreScrollState) override;
|
||||
|
||||
private:
|
||||
struct Element {
|
||||
not_null<HistoryItem*> item;
|
||||
uint64 started = 0;
|
||||
};
|
||||
const not_null<AbstractController*> _controller;
|
||||
|
||||
std::vector<Element> _elements;
|
||||
|
||||
std::unordered_map<not_null<HistoryItem*>, Media::CachedItem> _layouts;
|
||||
rpl::event_stream<not_null<Media::BaseLayout*>> _layoutRemoved;
|
||||
rpl::event_stream<> _refreshed;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Info::Downloads
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
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 "info/downloads/info_downloads_widget.h"
|
||||
|
||||
#include "info/downloads/info_downloads_inner_widget.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "ui/search_field_controller.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "data/data_download_manager.h"
|
||||
#include "data/data_user.h"
|
||||
#include "core/application.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Info::Downloads {
|
||||
|
||||
Memento::Memento(not_null<Controller*> controller)
|
||||
: ContentMemento(Tag{})
|
||||
, _media(controller) {
|
||||
}
|
||||
|
||||
Memento::Memento(not_null<UserData*> self)
|
||||
: ContentMemento(Downloads::Tag{})
|
||||
, _media(self, 0, Media::Type::File) {
|
||||
}
|
||||
|
||||
Memento::~Memento() = default;
|
||||
|
||||
Section Memento::section() const {
|
||||
return Section(Section::Type::Downloads);
|
||||
}
|
||||
|
||||
object_ptr<ContentWidget> Memento::createWidget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller,
|
||||
const QRect &geometry) {
|
||||
auto result = object_ptr<Widget>(parent, controller);
|
||||
result->setInternalState(geometry, this);
|
||||
return result;
|
||||
}
|
||||
|
||||
Widget::Widget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller)
|
||||
: ContentWidget(parent, controller) {
|
||||
_inner = setInnerWidget(object_ptr<InnerWidget>(
|
||||
this,
|
||||
controller));
|
||||
}
|
||||
|
||||
bool Widget::showInternal(not_null<ContentMemento*> memento) {
|
||||
if (!controller()->validateMementoPeer(memento)) {
|
||||
return false;
|
||||
}
|
||||
if (auto downloadsMemento = dynamic_cast<Memento*>(memento.get())) {
|
||||
restoreState(downloadsMemento);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Widget::setInternalState(
|
||||
const QRect &geometry,
|
||||
not_null<Memento*> memento) {
|
||||
setGeometry(geometry);
|
||||
Ui::SendPendingMoveResizeEvents(this);
|
||||
restoreState(memento);
|
||||
}
|
||||
|
||||
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
|
||||
auto result = std::make_shared<Memento>(controller());
|
||||
saveState(result.get());
|
||||
return result;
|
||||
}
|
||||
|
||||
void Widget::saveState(not_null<Memento*> memento) {
|
||||
memento->setScrollTop(scrollTopSave());
|
||||
_inner->saveState(memento);
|
||||
}
|
||||
|
||||
void Widget::restoreState(not_null<Memento*> memento) {
|
||||
_inner->restoreState(memento);
|
||||
scrollTopRestore(memento->scrollTop());
|
||||
}
|
||||
|
||||
std::shared_ptr<Info::Memento> Make(not_null<UserData*> self) {
|
||||
return std::make_shared<Info::Memento>(
|
||||
std::vector<std::shared_ptr<ContentMemento>>(
|
||||
1,
|
||||
std::make_shared<Memento>(self)));
|
||||
}
|
||||
|
||||
} // namespace Info::Downloads
|
||||
|
69
Telegram/SourceFiles/info/downloads/info_downloads_widget.h
Normal file
69
Telegram/SourceFiles/info/downloads/info_downloads_widget.h
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
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 "info/info_content_widget.h"
|
||||
#include "info/media/info_media_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
class SearchFieldController;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Info::Downloads {
|
||||
|
||||
class InnerWidget;
|
||||
|
||||
class Memento final : public ContentMemento {
|
||||
public:
|
||||
Memento(not_null<Controller*> controller);
|
||||
Memento(not_null<UserData*> self);
|
||||
~Memento();
|
||||
|
||||
object_ptr<ContentWidget> createWidget(
|
||||
QWidget *parent,
|
||||
not_null<Controller*> controller,
|
||||
const QRect &geometry) override;
|
||||
|
||||
Section section() const override;
|
||||
|
||||
[[nodiscard]] Media::Memento &media() {
|
||||
return _media;
|
||||
}
|
||||
[[nodiscard]] const Media::Memento &media() const {
|
||||
return _media;
|
||||
}
|
||||
|
||||
private:
|
||||
Media::Memento _media;
|
||||
|
||||
};
|
||||
|
||||
class Widget final : public ContentWidget {
|
||||
public:
|
||||
Widget(QWidget *parent, not_null<Controller*> controller);
|
||||
|
||||
bool showInternal(
|
||||
not_null<ContentMemento*> memento) override;
|
||||
|
||||
void setInternalState(
|
||||
const QRect &geometry,
|
||||
not_null<Memento*> memento);
|
||||
|
||||
private:
|
||||
void saveState(not_null<Memento*> memento);
|
||||
void restoreState(not_null<Memento*> memento);
|
||||
|
||||
std::shared_ptr<ContentMemento> doCreateMemento() override;
|
||||
|
||||
InnerWidget *_inner = nullptr;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<UserData*> self);
|
||||
|
||||
} // namespace Info::Downloads
|
|
@ -262,8 +262,10 @@ Key ContentMemento::key() const {
|
|||
return Key(peer);
|
||||
} else if (const auto poll = this->poll()) {
|
||||
return Key(poll, pollContextId());
|
||||
} else if (const auto self = settingsSelf()) {
|
||||
return Settings::Tag{ self };
|
||||
} else {
|
||||
return Settings::Tag{ settingsSelf() };
|
||||
return Downloads::Tag();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,4 +273,7 @@ ContentMemento::ContentMemento(Settings::Tag settings)
|
|||
: _settingsSelf(settings.self.get()) {
|
||||
}
|
||||
|
||||
ContentMemento::ContentMemento(Downloads::Tag downloads) {
|
||||
}
|
||||
|
||||
} // namespace Info
|
||||
|
|
|
@ -28,6 +28,10 @@ namespace Settings {
|
|||
struct Tag;
|
||||
} // namespace Settings
|
||||
|
||||
namespace Downloads {
|
||||
struct Tag;
|
||||
} // namespace Downloads
|
||||
|
||||
class ContentMemento;
|
||||
class Controller;
|
||||
|
||||
|
@ -120,6 +124,7 @@ public:
|
|||
, _migratedPeerId(migratedPeerId) {
|
||||
}
|
||||
explicit ContentMemento(Settings::Tag settings);
|
||||
explicit ContentMemento(Downloads::Tag downloads);
|
||||
ContentMemento(not_null<PollData*> poll, FullMsgId contextId)
|
||||
: _poll(poll)
|
||||
, _pollContextId(contextId) {
|
||||
|
|
|
@ -14,13 +14,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "info/info_content_widget.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "info/media/info_media_widget.h"
|
||||
#include "core/application.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_download_manager.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history.h"
|
||||
#include "main/main_session.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
|
@ -32,6 +35,9 @@ Key::Key(not_null<PeerData*> peer) : _value(peer) {
|
|||
Key::Key(Settings::Tag settings) : _value(settings) {
|
||||
}
|
||||
|
||||
Key::Key(Downloads::Tag downloads) : _value(downloads) {
|
||||
}
|
||||
|
||||
Key::Key(not_null<PollData*> poll, FullMsgId contextId)
|
||||
: _value(PollKey{ poll, contextId }) {
|
||||
}
|
||||
|
@ -50,6 +56,10 @@ UserData *Key::settingsSelf() const {
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
bool Key::isDownloads() const {
|
||||
return v::is<Downloads::Tag>(_value);
|
||||
}
|
||||
|
||||
PollData *Key::poll() const {
|
||||
if (const auto data = std::get_if<PollKey>(&_value)) {
|
||||
return data->poll;
|
||||
|
@ -98,6 +108,28 @@ rpl::producer<QString> AbstractController::mediaSourceQueryValue() const {
|
|||
return rpl::single(QString());
|
||||
}
|
||||
|
||||
rpl::producer<DownloadsSlice> AbstractController::downloadsSource() const {
|
||||
const auto manager = &Core::App().downloadManager();
|
||||
return rpl::single(
|
||||
rpl::empty_value()
|
||||
) | rpl::then(
|
||||
manager->loadingListChanges()
|
||||
) | rpl::map([=] {
|
||||
auto result = DownloadsSlice();
|
||||
for (const auto &id : manager->loadingList()) {
|
||||
result.entries.push_back(DownloadsEntry{
|
||||
.item = id.object.item,
|
||||
.started = id.started,
|
||||
});
|
||||
}
|
||||
ranges::sort(
|
||||
result.entries,
|
||||
ranges::less(),
|
||||
&DownloadsEntry::started);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
AbstractController::AbstractController(
|
||||
not_null<Window::SessionController*> parent)
|
||||
: SessionNavigation(&parent->session())
|
||||
|
@ -204,17 +236,19 @@ void Controller::setSection(not_null<ContentMemento*> memento) {
|
|||
void Controller::updateSearchControllers(
|
||||
not_null<ContentMemento*> memento) {
|
||||
using Type = Section::Type;
|
||||
auto type = _section.type();
|
||||
auto isMedia = (type == Type::Media);
|
||||
auto mediaType = isMedia
|
||||
const auto type = _section.type();
|
||||
const auto isMedia = (type == Type::Media);
|
||||
const auto mediaType = isMedia
|
||||
? _section.mediaType()
|
||||
: Section::MediaType::kCount;
|
||||
auto hasMediaSearch = isMedia
|
||||
const auto hasMediaSearch = isMedia
|
||||
&& SharedMediaAllowSearch(mediaType);
|
||||
auto hasCommonGroupsSearch
|
||||
const auto hasCommonGroupsSearch
|
||||
= (type == Type::CommonGroups);
|
||||
auto hasMembersSearch = (type == Type::Members || type == Type::Profile);
|
||||
auto searchQuery = memento->searchFieldQuery();
|
||||
const auto hasMembersSearch = (type == Type::Members)
|
||||
|| (type == Type::Profile);
|
||||
const auto searchQuery = memento->searchFieldQuery();
|
||||
const auto isDownloads = (type == Type::Downloads);
|
||||
if (isMedia) {
|
||||
_searchController
|
||||
= std::make_unique<Api::DelayedSearchController>(&session());
|
||||
|
@ -289,7 +323,9 @@ rpl::producer<bool> Controller::searchEnabledByContent() const {
|
|||
}
|
||||
|
||||
rpl::producer<QString> Controller::mediaSourceQueryValue() const {
|
||||
return _searchController->currentQueryValue();
|
||||
return _searchController
|
||||
? _searchController->currentQueryValue()
|
||||
: rpl::never<QString>(); AssertIsDebug() // #TODO downloads
|
||||
}
|
||||
|
||||
rpl::producer<SparseIdsMergedSlice> Controller::mediaSource(
|
||||
|
|
|
@ -28,14 +28,23 @@ struct Tag {
|
|||
|
||||
} // namespace Settings
|
||||
|
||||
namespace Downloads {
|
||||
|
||||
struct Tag {
|
||||
};
|
||||
|
||||
} // namespace Downloads
|
||||
|
||||
class Key {
|
||||
public:
|
||||
Key(not_null<PeerData*> peer);
|
||||
Key(Settings::Tag settings);
|
||||
Key(Downloads::Tag downloads);
|
||||
Key(not_null<PollData*> poll, FullMsgId contextId);
|
||||
|
||||
PeerData *peer() const;
|
||||
UserData *settingsSelf() const;
|
||||
bool isDownloads() const;
|
||||
PollData *poll() const;
|
||||
FullMsgId pollContextId() const;
|
||||
|
||||
|
@ -47,6 +56,7 @@ private:
|
|||
std::variant<
|
||||
not_null<PeerData*>,
|
||||
Settings::Tag,
|
||||
Downloads::Tag,
|
||||
PollKey> _value;
|
||||
|
||||
};
|
||||
|
@ -64,6 +74,7 @@ public:
|
|||
CommonGroups,
|
||||
Members,
|
||||
Settings,
|
||||
Downloads,
|
||||
PollResults,
|
||||
};
|
||||
using SettingsType = ::Settings::Type;
|
||||
|
@ -102,6 +113,14 @@ private:
|
|||
|
||||
};
|
||||
|
||||
struct DownloadsEntry {
|
||||
not_null<HistoryItem*> item;
|
||||
int64 started = 0; // unixtime * 1000.
|
||||
};
|
||||
struct DownloadsSlice {
|
||||
std::vector<DownloadsEntry> entries;
|
||||
};
|
||||
|
||||
class AbstractController : public Window::SessionNavigation {
|
||||
public:
|
||||
AbstractController(not_null<Window::SessionController*> parent);
|
||||
|
@ -115,6 +134,9 @@ public:
|
|||
UserData *settingsSelf() const {
|
||||
return key().settingsSelf();
|
||||
}
|
||||
bool isDownloads() const {
|
||||
return key().isDownloads();
|
||||
}
|
||||
PollData *poll() const;
|
||||
FullMsgId pollContextId() const {
|
||||
return key().pollContextId();
|
||||
|
@ -128,6 +150,8 @@ public:
|
|||
int limitAfter) const;
|
||||
virtual rpl::producer<QString> mediaSourceQueryValue() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<DownloadsSlice> downloadsSource() const;
|
||||
|
||||
void showSection(
|
||||
std::shared_ptr<Window::SectionMemento> memento,
|
||||
const Window::SectionShow ¶ms = Window::SectionShow()) override;
|
||||
|
|
|
@ -27,6 +27,10 @@ namespace Settings {
|
|||
struct Tag;
|
||||
} // namespace Settings
|
||||
|
||||
namespace Downloads {
|
||||
struct Tag;
|
||||
} // namespace Downloads
|
||||
|
||||
class ContentMemento;
|
||||
class WrapWidget;
|
||||
|
||||
|
|
|
@ -545,9 +545,13 @@ MessageIdsList TopBar::collectItems() const {
|
|||
return ranges::views::all(
|
||||
_selectedItems.list
|
||||
) | ranges::views::transform([](auto &&item) {
|
||||
return item.msgId;
|
||||
}) | ranges::views::filter([&](FullMsgId msgId) {
|
||||
return _navigation->session().data().message(msgId) != nullptr;
|
||||
return item.globalId;
|
||||
}) | ranges::views::filter([&](const GlobalMsgId &globalId) {
|
||||
const auto session = &_navigation->session();
|
||||
return (globalId.sessionUniqueId == session->uniqueId())
|
||||
&& (session->data().message(globalId.itemId) != nullptr);
|
||||
}) | ranges::views::transform([](const GlobalMsgId &globalId) {
|
||||
return globalId.itemId;
|
||||
}) | ranges::to_vector;
|
||||
}
|
||||
|
||||
|
@ -568,6 +572,7 @@ void TopBar::performForward() {
|
|||
}
|
||||
|
||||
void TopBar::performDelete() {
|
||||
// #TODO downloads
|
||||
auto items = collectItems();
|
||||
if (items.empty()) {
|
||||
_cancelSelectionClicks.fire({});
|
||||
|
@ -667,6 +672,10 @@ rpl::producer<QString> TitleValue(
|
|||
return key.poll()->quiz()
|
||||
? tr::lng_polls_quiz_results_title()
|
||||
: tr::lng_polls_poll_results_title();
|
||||
|
||||
case Section::Type::Downloads:
|
||||
return rpl::single(u"Downloads"_q);
|
||||
|
||||
}
|
||||
Unexpected("Bad section type in Info::TitleValue()");
|
||||
}
|
||||
|
|
|
@ -871,13 +871,19 @@ void WrapWidget::showNewContent(
|
|||
_historyStack.clear();
|
||||
}
|
||||
|
||||
_controller = std::move(newController);
|
||||
if (newContent) {
|
||||
setupTop();
|
||||
showContent(std::move(newContent));
|
||||
} else {
|
||||
showNewContent(memento);
|
||||
{
|
||||
// Let old controller outlive old content widget.
|
||||
const auto oldController = std::exchange(
|
||||
_controller,
|
||||
std::move(newController));
|
||||
if (newContent) {
|
||||
setupTop();
|
||||
showContent(std::move(newContent));
|
||||
} else {
|
||||
showNewContent(memento);
|
||||
}
|
||||
}
|
||||
|
||||
if (animationParams) {
|
||||
if (Ui::InFocusChain(this)) {
|
||||
setFocus();
|
||||
|
|
|
@ -51,10 +51,10 @@ enum class Wrap {
|
|||
};
|
||||
|
||||
struct SelectedItem {
|
||||
explicit SelectedItem(FullMsgId msgId) : msgId(msgId) {
|
||||
explicit SelectedItem(GlobalMsgId globalId) : globalId(globalId) {
|
||||
}
|
||||
|
||||
FullMsgId msgId;
|
||||
GlobalMsgId globalId;
|
||||
bool canDelete = false;
|
||||
bool canForward = false;
|
||||
};
|
||||
|
|
|
@ -23,8 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_user.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Info {
|
||||
namespace Media {
|
||||
namespace Info::Media {
|
||||
|
||||
using Type = Storage::SharedMediaType;
|
||||
|
||||
|
@ -118,5 +117,4 @@ inline auto AddCommonGroupsButton(
|
|||
return result;
|
||||
};
|
||||
|
||||
} // namespace Media
|
||||
} // namespace Info
|
||||
} // namespace Info::Media
|
||||
|
|
54
Telegram/SourceFiles/info/media/info_media_common.cpp
Normal file
54
Telegram/SourceFiles/info/media/info_media_common.cpp
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
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 "info/media/info_media_common.h"
|
||||
|
||||
#include "history/history_item.h"
|
||||
|
||||
namespace Info::Media {
|
||||
|
||||
UniversalMsgId GetUniversalId(FullMsgId itemId) {
|
||||
return peerIsChannel(itemId.peer)
|
||||
? UniversalMsgId(itemId.msg)
|
||||
: UniversalMsgId(itemId.msg - ServerMaxMsgId);
|
||||
}
|
||||
|
||||
UniversalMsgId GetUniversalId(not_null<const HistoryItem*> item) {
|
||||
return GetUniversalId(item->fullId());
|
||||
}
|
||||
|
||||
UniversalMsgId GetUniversalId(not_null<const BaseLayout*> layout) {
|
||||
return GetUniversalId(layout->getItem()->fullId());
|
||||
}
|
||||
|
||||
bool ChangeItemSelection(
|
||||
ListSelectedMap &selected,
|
||||
not_null<const HistoryItem*> item,
|
||||
TextSelection selection) {
|
||||
const auto changeExisting = [&](auto it) {
|
||||
if (it == selected.cend()) {
|
||||
return false;
|
||||
} else if (it->second.text != selection) {
|
||||
it->second.text = selection;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (selected.size() < MaxSelectedItems) {
|
||||
const auto [i, ok] = selected.try_emplace(item, selection);
|
||||
if (ok) {
|
||||
// #TODO downloads
|
||||
i->second.canDelete = item->canDelete();
|
||||
i->second.canForward = item->allowsForward();
|
||||
return true;
|
||||
}
|
||||
return changeExisting(i);
|
||||
}
|
||||
return changeExisting(selected.find(item));
|
||||
}
|
||||
|
||||
} // namespace Info::Media
|
139
Telegram/SourceFiles/info/media/info_media_common.h
Normal file
139
Telegram/SourceFiles/info/media/info_media_common.h
Normal file
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
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 "overview/overview_layout.h"
|
||||
|
||||
namespace Storage {
|
||||
enum class SharedMediaType : signed char;
|
||||
} // namespace Storage
|
||||
|
||||
namespace Info::Media {
|
||||
|
||||
using Type = Storage::SharedMediaType;
|
||||
using BaseLayout = Overview::Layout::ItemBase;
|
||||
|
||||
class Memento;
|
||||
class ListSection;
|
||||
|
||||
inline constexpr auto kPreloadIfLessThanScreens = 2;
|
||||
|
||||
struct ListItemSelectionData {
|
||||
explicit ListItemSelectionData(TextSelection text) : text(text) {
|
||||
}
|
||||
|
||||
TextSelection text;
|
||||
bool canDelete = false;
|
||||
bool canForward = false;
|
||||
};
|
||||
|
||||
using ListSelectedMap = base::flat_map<
|
||||
not_null<const HistoryItem*>,
|
||||
ListItemSelectionData,
|
||||
std::less<>>;
|
||||
|
||||
enum class ListDragSelectAction {
|
||||
None,
|
||||
Selecting,
|
||||
Deselecting,
|
||||
};
|
||||
|
||||
struct ListContext {
|
||||
Overview::Layout::PaintContext layoutContext;
|
||||
not_null<ListSelectedMap*> selected;
|
||||
not_null<ListSelectedMap*> dragSelected;
|
||||
ListDragSelectAction dragSelectAction = ListDragSelectAction::None;
|
||||
};
|
||||
|
||||
struct ListScrollTopState {
|
||||
HistoryItem *item = nullptr;
|
||||
int shift = 0;
|
||||
};
|
||||
|
||||
struct ListFoundItem {
|
||||
not_null<BaseLayout*> layout;
|
||||
QRect geometry;
|
||||
bool exact = false;
|
||||
};
|
||||
|
||||
struct CachedItem {
|
||||
CachedItem(std::unique_ptr<BaseLayout> item) : item(std::move(item)) {
|
||||
};
|
||||
CachedItem(CachedItem &&other) = default;
|
||||
CachedItem &operator=(CachedItem &&other) = default;
|
||||
~CachedItem() = default;
|
||||
|
||||
std::unique_ptr<BaseLayout> item;
|
||||
bool stale = false;
|
||||
};
|
||||
|
||||
using UniversalMsgId = MsgId;
|
||||
|
||||
[[nodiscard]] UniversalMsgId GetUniversalId(FullMsgId itemId);
|
||||
[[nodiscard]] UniversalMsgId GetUniversalId(
|
||||
not_null<const HistoryItem*> item);
|
||||
[[nodiscard]] UniversalMsgId GetUniversalId(
|
||||
not_null<const BaseLayout*> layout);
|
||||
|
||||
bool ChangeItemSelection(
|
||||
ListSelectedMap &selected,
|
||||
not_null<const HistoryItem*> item,
|
||||
TextSelection selection);
|
||||
|
||||
class ListProvider {
|
||||
public:
|
||||
[[nodiscard]] virtual Type type() = 0;
|
||||
[[nodiscard]] virtual bool hasSelectRestriction() = 0;
|
||||
[[nodiscard]] virtual auto hasSelectRestrictionChanges()
|
||||
->rpl::producer<bool> = 0;
|
||||
[[nodiscard]] virtual bool isPossiblyMyItem(
|
||||
not_null<const HistoryItem*> item) = 0;
|
||||
|
||||
[[nodiscard]] virtual std::optional<int> fullCount() = 0;
|
||||
|
||||
virtual void restart() = 0;
|
||||
virtual void checkPreload(
|
||||
QSize viewport,
|
||||
not_null<BaseLayout*> topLayout,
|
||||
not_null<BaseLayout*> bottomLayout,
|
||||
bool preloadTop,
|
||||
bool preloadBottom) = 0;
|
||||
virtual void refreshViewer() = 0;
|
||||
[[nodiscard]] virtual rpl::producer<> refreshed() = 0;
|
||||
|
||||
[[nodiscard]] virtual std::vector<ListSection> fillSections(
|
||||
not_null<Overview::Layout::Delegate*> delegate) = 0;
|
||||
[[nodiscard]] virtual auto layoutRemoved()
|
||||
-> rpl::producer<not_null<BaseLayout*>> = 0;
|
||||
[[nodiscard]] virtual BaseLayout *lookupLayout(
|
||||
const HistoryItem *item) = 0;
|
||||
[[nodiscard]] virtual bool isMyItem(
|
||||
not_null<const HistoryItem*> item) = 0;
|
||||
[[nodiscard]] virtual bool isAfter(
|
||||
not_null<const HistoryItem*> a,
|
||||
not_null<const HistoryItem*> b) = 0;
|
||||
|
||||
virtual void applyDragSelection(
|
||||
ListSelectedMap &selected,
|
||||
not_null<const HistoryItem*> fromItem,
|
||||
bool skipFrom,
|
||||
not_null<const HistoryItem*> tillItem,
|
||||
bool skipTill) = 0;
|
||||
|
||||
virtual void saveState(
|
||||
not_null<Memento*> memento,
|
||||
ListScrollTopState scrollState) = 0;
|
||||
virtual void restoreState(
|
||||
not_null<Memento*> memento,
|
||||
Fn<void(ListScrollTopState)> restoreScrollState) = 0;
|
||||
|
||||
virtual ~ListProvider() = default;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Info::Media
|
486
Telegram/SourceFiles/info/media/info_media_list_section.cpp
Normal file
486
Telegram/SourceFiles/info/media/info_media_list_section.cpp
Normal file
|
@ -0,0 +1,486 @@
|
|||
/*
|
||||
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 "info/media/info_media_list_section.h"
|
||||
|
||||
#include "storage/storage_shared_media.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "layout/layout_selection.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Info::Media {
|
||||
namespace {
|
||||
|
||||
constexpr auto kFloatingHeaderAlpha = 0.9;
|
||||
|
||||
[[nodiscard]] bool HasFloatingHeader(Type type) {
|
||||
switch (type) {
|
||||
case Type::Photo:
|
||||
case Type::GIF:
|
||||
case Type::Video:
|
||||
case Type::RoundFile:
|
||||
case Type::RoundVoiceFile:
|
||||
case Type::MusicFile:
|
||||
return false;
|
||||
case Type::File:
|
||||
case Type::Link:
|
||||
return true;
|
||||
}
|
||||
Unexpected("Type in HasFloatingHeader()");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ListSection::ListSection(Type type)
|
||||
: _type(type)
|
||||
, _hasFloatingHeader(HasFloatingHeader(type))
|
||||
, _mosaic(st::emojiPanWidth - st::inlineResultsLeft) {
|
||||
}
|
||||
|
||||
bool ListSection::empty() const {
|
||||
return _items.empty();
|
||||
}
|
||||
|
||||
UniversalMsgId ListSection::minId() const {
|
||||
Expects(!empty());
|
||||
|
||||
return GetUniversalId(_items.back()->getItem());
|
||||
}
|
||||
|
||||
void ListSection::setTop(int top) {
|
||||
_top = top;
|
||||
}
|
||||
|
||||
int ListSection::top() const {
|
||||
return _top;
|
||||
}
|
||||
|
||||
int ListSection::height() const {
|
||||
return _height;
|
||||
}
|
||||
|
||||
int ListSection::bottom() const {
|
||||
return top() + height();
|
||||
}
|
||||
|
||||
bool ListSection::addItem(not_null<BaseLayout*> item) {
|
||||
if (_items.empty() || belongsHere(item)) {
|
||||
if (_items.empty()) {
|
||||
setHeader(item);
|
||||
}
|
||||
appendItem(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ListSection::finishSection() {
|
||||
if (_type == Type::GIF) {
|
||||
_mosaic.setOffset(st::infoMediaSkip, headerHeight());
|
||||
_mosaic.setRightSkip(st::infoMediaSkip);
|
||||
_mosaic.addItems(_items);
|
||||
}
|
||||
}
|
||||
|
||||
void ListSection::setHeader(not_null<BaseLayout*> item) {
|
||||
auto text = [&] {
|
||||
auto date = item->dateTime().date();
|
||||
switch (_type) {
|
||||
case Type::Photo:
|
||||
case Type::GIF:
|
||||
case Type::Video:
|
||||
case Type::RoundFile:
|
||||
case Type::RoundVoiceFile:
|
||||
case Type::File:
|
||||
return langMonthFull(date);
|
||||
|
||||
case Type::Link:
|
||||
return langDayOfMonthFull(date);
|
||||
|
||||
case Type::MusicFile:
|
||||
return QString();
|
||||
}
|
||||
Unexpected("Type in ListSection::setHeader()");
|
||||
}();
|
||||
_header.setText(st::infoMediaHeaderStyle, text);
|
||||
}
|
||||
|
||||
bool ListSection::belongsHere(
|
||||
not_null<BaseLayout*> item) const {
|
||||
Expects(!_items.empty());
|
||||
|
||||
auto date = item->dateTime().date();
|
||||
auto myDate = _items.back()->dateTime().date();
|
||||
|
||||
switch (_type) {
|
||||
case Type::Photo:
|
||||
case Type::GIF:
|
||||
case Type::Video:
|
||||
case Type::RoundFile:
|
||||
case Type::RoundVoiceFile:
|
||||
case Type::File:
|
||||
return date.year() == myDate.year()
|
||||
&& date.month() == myDate.month();
|
||||
|
||||
case Type::Link:
|
||||
return date.year() == myDate.year()
|
||||
&& date.month() == myDate.month()
|
||||
&& date.day() == myDate.day();
|
||||
|
||||
case Type::MusicFile:
|
||||
return true;
|
||||
}
|
||||
Unexpected("Type in ListSection::belongsHere()");
|
||||
}
|
||||
|
||||
void ListSection::appendItem(not_null<BaseLayout*> item) {
|
||||
_items.push_back(item);
|
||||
_byItem.emplace(item->getItem(), item);
|
||||
}
|
||||
|
||||
bool ListSection::removeItem(not_null<const HistoryItem*> item) {
|
||||
if (const auto i = _byItem.find(item); i != end(_byItem)) {
|
||||
_items.erase(ranges::remove(_items, i->second), end(_items));
|
||||
_byItem.erase(i);
|
||||
refreshHeight();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QRect ListSection::findItemRect(
|
||||
not_null<const BaseLayout*> item) const {
|
||||
auto position = item->position();
|
||||
if (!_mosaic.empty()) {
|
||||
return _mosaic.findRect(position);
|
||||
}
|
||||
auto top = position / _itemsInRow;
|
||||
auto indexInRow = position % _itemsInRow;
|
||||
auto left = _itemsLeft
|
||||
+ indexInRow * (_itemWidth + st::infoMediaSkip);
|
||||
return QRect(left, top, _itemWidth, item->height());
|
||||
}
|
||||
|
||||
ListFoundItem ListSection::completeResult(
|
||||
not_null<BaseLayout*> item,
|
||||
bool exact) const {
|
||||
return { item, findItemRect(item), exact };
|
||||
}
|
||||
|
||||
ListFoundItem ListSection::findItemByPoint(QPoint point) const {
|
||||
Expects(!_items.empty());
|
||||
|
||||
if (!_mosaic.empty()) {
|
||||
const auto found = _mosaic.findByPoint(point);
|
||||
Assert(found.index != -1);
|
||||
const auto item = _mosaic.itemAt(found.index);
|
||||
const auto rect = findItemRect(item);
|
||||
return { item, rect, found.exact };
|
||||
}
|
||||
auto itemIt = findItemAfterTop(point.y());
|
||||
if (itemIt == end(_items)) {
|
||||
--itemIt;
|
||||
}
|
||||
auto item = *itemIt;
|
||||
auto rect = findItemRect(item);
|
||||
if (point.y() >= rect.top()) {
|
||||
auto shift = floorclamp(
|
||||
point.x(),
|
||||
(_itemWidth + st::infoMediaSkip),
|
||||
0,
|
||||
_itemsInRow);
|
||||
while (shift-- && itemIt != _items.end()) {
|
||||
++itemIt;
|
||||
}
|
||||
if (itemIt == _items.end()) {
|
||||
--itemIt;
|
||||
}
|
||||
item = *itemIt;
|
||||
rect = findItemRect(item);
|
||||
}
|
||||
return { item, rect, rect.contains(point) };
|
||||
}
|
||||
|
||||
std::optional<ListFoundItem> ListSection::findItemByItem(
|
||||
not_null<const HistoryItem*> item) const {
|
||||
const auto i = _byItem.find(item);
|
||||
if (i != end(_byItem)) {
|
||||
return ListFoundItem{ i->second, findItemRect(i->second), true };
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ListFoundItem ListSection::findItemNearId(UniversalMsgId universalId) const {
|
||||
Expects(!_items.empty());
|
||||
|
||||
// #TODO downloads
|
||||
auto itemIt = ranges::lower_bound(
|
||||
_items,
|
||||
universalId,
|
||||
std::greater<>(),
|
||||
[](const auto &item) { return GetUniversalId(item); });
|
||||
if (itemIt == _items.end()) {
|
||||
--itemIt;
|
||||
}
|
||||
const auto item = *itemIt;
|
||||
const auto exact = (GetUniversalId(item) == universalId);
|
||||
return { item, findItemRect(item), exact };
|
||||
}
|
||||
|
||||
ListFoundItem ListSection::findItemDetails(
|
||||
not_null<BaseLayout*> item) const {
|
||||
return { item, findItemRect(item), true };
|
||||
}
|
||||
|
||||
auto ListSection::findItemAfterTop(
|
||||
int top) -> Items::iterator {
|
||||
Expects(_mosaic.empty());
|
||||
|
||||
return ranges::lower_bound(
|
||||
_items,
|
||||
top,
|
||||
std::less_equal<>(),
|
||||
[this](const auto &item) {
|
||||
const auto itemTop = item->position() / _itemsInRow;
|
||||
return itemTop + item->height();
|
||||
});
|
||||
}
|
||||
|
||||
auto ListSection::findItemAfterTop(
|
||||
int top) const -> Items::const_iterator {
|
||||
Expects(_mosaic.empty());
|
||||
|
||||
return ranges::lower_bound(
|
||||
_items,
|
||||
top,
|
||||
std::less_equal<>(),
|
||||
[this](const auto &item) {
|
||||
const auto itemTop = item->position() / _itemsInRow;
|
||||
return itemTop + item->height();
|
||||
});
|
||||
}
|
||||
|
||||
auto ListSection::findItemAfterBottom(
|
||||
Items::const_iterator from,
|
||||
int bottom) const -> Items::const_iterator {
|
||||
Expects(_mosaic.empty());
|
||||
return ranges::lower_bound(
|
||||
from,
|
||||
_items.end(),
|
||||
bottom,
|
||||
std::less<>(),
|
||||
[this](const auto &item) {
|
||||
const auto itemTop = item->position() / _itemsInRow;
|
||||
return itemTop;
|
||||
});
|
||||
}
|
||||
|
||||
void ListSection::paint(
|
||||
Painter &p,
|
||||
const ListContext &context,
|
||||
QRect clip,
|
||||
int outerWidth) const {
|
||||
auto header = headerHeight();
|
||||
if (QRect(0, 0, outerWidth, header).intersects(clip)) {
|
||||
p.setPen(st::infoMediaHeaderFg);
|
||||
_header.drawLeftElided(
|
||||
p,
|
||||
st::infoMediaHeaderPosition.x(),
|
||||
st::infoMediaHeaderPosition.y(),
|
||||
outerWidth - 2 * st::infoMediaHeaderPosition.x(),
|
||||
outerWidth);
|
||||
}
|
||||
auto localContext = context.layoutContext;
|
||||
localContext.isAfterDate = (header > 0);
|
||||
|
||||
if (!_mosaic.empty()) {
|
||||
auto paintItem = [&](not_null<BaseLayout*> item, QPoint point) {
|
||||
p.translate(point.x(), point.y());
|
||||
item->paint(
|
||||
p,
|
||||
clip.translated(-point),
|
||||
itemSelection(item, context),
|
||||
&localContext);
|
||||
p.translate(-point.x(), -point.y());
|
||||
};
|
||||
_mosaic.paint(std::move(paintItem), clip);
|
||||
return;
|
||||
}
|
||||
|
||||
auto fromIt = findItemAfterTop(clip.y());
|
||||
auto tillIt = findItemAfterBottom(
|
||||
fromIt,
|
||||
clip.y() + clip.height());
|
||||
for (auto it = fromIt; it != tillIt; ++it) {
|
||||
auto item = *it;
|
||||
auto rect = findItemRect(item);
|
||||
localContext.isAfterDate = (header > 0)
|
||||
&& (rect.y() <= header + _itemsTop);
|
||||
if (rect.intersects(clip)) {
|
||||
p.translate(rect.topLeft());
|
||||
item->paint(
|
||||
p,
|
||||
clip.translated(-rect.topLeft()),
|
||||
itemSelection(item, context),
|
||||
&localContext);
|
||||
p.translate(-rect.topLeft());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ListSection::paintFloatingHeader(
|
||||
Painter &p,
|
||||
int visibleTop,
|
||||
int outerWidth) {
|
||||
if (!_hasFloatingHeader) {
|
||||
return;
|
||||
}
|
||||
const auto headerTop = st::infoMediaHeaderPosition.y() / 2;
|
||||
if (visibleTop <= (_top + headerTop)) {
|
||||
return;
|
||||
}
|
||||
const auto header = headerHeight();
|
||||
const auto headerLeft = st::infoMediaHeaderPosition.x();
|
||||
const auto floatingTop = std::min(
|
||||
visibleTop,
|
||||
bottom() - header + headerTop);
|
||||
p.save();
|
||||
p.resetTransform();
|
||||
p.setOpacity(kFloatingHeaderAlpha);
|
||||
p.fillRect(QRect(0, floatingTop, outerWidth, header), st::boxBg);
|
||||
p.setOpacity(1.0);
|
||||
p.setPen(st::infoMediaHeaderFg);
|
||||
_header.drawLeftElided(
|
||||
p,
|
||||
headerLeft,
|
||||
floatingTop + headerTop,
|
||||
outerWidth - 2 * headerLeft,
|
||||
outerWidth);
|
||||
p.restore();
|
||||
}
|
||||
|
||||
TextSelection ListSection::itemSelection(
|
||||
not_null<const BaseLayout*> item,
|
||||
const ListContext &context) const {
|
||||
const auto parent = item->getItem();
|
||||
auto dragSelectAction = context.dragSelectAction;
|
||||
if (dragSelectAction != ListDragSelectAction::None) {
|
||||
auto i = context.dragSelected->find(parent);
|
||||
if (i != context.dragSelected->end()) {
|
||||
return (dragSelectAction == ListDragSelectAction::Selecting)
|
||||
? FullSelection
|
||||
: TextSelection();
|
||||
}
|
||||
}
|
||||
auto i = context.selected->find(parent);
|
||||
return (i == context.selected->cend())
|
||||
? TextSelection()
|
||||
: i->second.text;
|
||||
}
|
||||
|
||||
int ListSection::headerHeight() const {
|
||||
return _header.isEmpty() ? 0 : st::infoMediaHeaderHeight;
|
||||
}
|
||||
|
||||
void ListSection::resizeToWidth(int newWidth) {
|
||||
auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2;
|
||||
if (newWidth < minWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto resizeOneColumn = [&](int itemsLeft, int itemWidth) {
|
||||
_itemsLeft = itemsLeft;
|
||||
_itemsTop = 0;
|
||||
_itemsInRow = 1;
|
||||
_itemWidth = itemWidth;
|
||||
for (auto &item : _items) {
|
||||
item->resizeGetHeight(_itemWidth);
|
||||
}
|
||||
};
|
||||
switch (_type) {
|
||||
case Type::Photo:
|
||||
case Type::Video:
|
||||
case Type::RoundFile: {
|
||||
_itemsLeft = st::infoMediaSkip;
|
||||
_itemsTop = st::infoMediaSkip;
|
||||
_itemsInRow = (newWidth - _itemsLeft)
|
||||
/ (st::infoMediaMinGridSize + st::infoMediaSkip);
|
||||
_itemWidth = ((newWidth - _itemsLeft) / _itemsInRow)
|
||||
- st::infoMediaSkip;
|
||||
for (auto &item : _items) {
|
||||
item->resizeGetHeight(_itemWidth);
|
||||
}
|
||||
} break;
|
||||
|
||||
case Type::GIF: {
|
||||
_mosaic.setFullWidth(newWidth - st::infoMediaSkip);
|
||||
} break;
|
||||
|
||||
case Type::RoundVoiceFile:
|
||||
case Type::MusicFile:
|
||||
resizeOneColumn(0, newWidth);
|
||||
break;
|
||||
case Type::File:
|
||||
case Type::Link: {
|
||||
auto itemsLeft = st::infoMediaHeaderPosition.x();
|
||||
auto itemWidth = newWidth - 2 * itemsLeft;
|
||||
resizeOneColumn(itemsLeft, itemWidth);
|
||||
} break;
|
||||
}
|
||||
|
||||
refreshHeight();
|
||||
}
|
||||
|
||||
int ListSection::recountHeight() {
|
||||
auto result = headerHeight();
|
||||
|
||||
switch (_type) {
|
||||
case Type::Photo:
|
||||
case Type::Video:
|
||||
case Type::RoundFile: {
|
||||
auto itemHeight = _itemWidth + st::infoMediaSkip;
|
||||
auto index = 0;
|
||||
result += _itemsTop;
|
||||
for (auto &item : _items) {
|
||||
item->setPosition(_itemsInRow * result + index);
|
||||
if (++index == _itemsInRow) {
|
||||
result += itemHeight;
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
if (_items.size() % _itemsInRow) {
|
||||
_rowsCount = int(_items.size()) / _itemsInRow + 1;
|
||||
result += itemHeight;
|
||||
} else {
|
||||
_rowsCount = int(_items.size()) / _itemsInRow;
|
||||
}
|
||||
} break;
|
||||
|
||||
case Type::GIF: {
|
||||
return _mosaic.countDesiredHeight(0) + result;
|
||||
} break;
|
||||
|
||||
case Type::RoundVoiceFile:
|
||||
case Type::File:
|
||||
case Type::MusicFile:
|
||||
case Type::Link:
|
||||
for (auto &item : _items) {
|
||||
item->setPosition(result);
|
||||
result += item->height();
|
||||
}
|
||||
_rowsCount = _items.size();
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ListSection::refreshHeight() {
|
||||
_height = recountHeight();
|
||||
}
|
||||
|
||||
} // namespace Info::Media
|
93
Telegram/SourceFiles/info/media/info_media_list_section.h
Normal file
93
Telegram/SourceFiles/info/media/info_media_list_section.h
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
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 "info/media/info_media_common.h"
|
||||
#include "layout/layout_mosaic.h"
|
||||
#include "ui/text/text.h"
|
||||
|
||||
namespace Info::Media {
|
||||
|
||||
class ListSection {
|
||||
public:
|
||||
ListSection(Type type);
|
||||
|
||||
bool addItem(not_null<BaseLayout*> item);
|
||||
void finishSection();
|
||||
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
[[nodiscard]] UniversalMsgId minId() const;
|
||||
|
||||
void setTop(int top);
|
||||
[[nodiscard]] int top() const;
|
||||
void resizeToWidth(int newWidth);
|
||||
[[nodiscard]] int height() const;
|
||||
|
||||
[[nodiscard]] int bottom() const;
|
||||
|
||||
bool removeItem(not_null<const HistoryItem*> item);
|
||||
[[nodiscard]] std::optional<ListFoundItem> findItemByItem(
|
||||
not_null<const HistoryItem*> item) const;
|
||||
[[nodiscard]] ListFoundItem findItemNearId(
|
||||
UniversalMsgId universalId) const;
|
||||
[[nodiscard]] ListFoundItem findItemDetails(
|
||||
not_null<BaseLayout*> item) const;
|
||||
[[nodiscard]] ListFoundItem findItemByPoint(QPoint point) const;
|
||||
|
||||
void paint(
|
||||
Painter &p,
|
||||
const ListContext &context,
|
||||
QRect clip,
|
||||
int outerWidth) const;
|
||||
|
||||
void paintFloatingHeader(Painter &p, int visibleTop, int outerWidth);
|
||||
|
||||
private:
|
||||
using Items = std::vector<not_null<BaseLayout*>>;
|
||||
|
||||
[[nodiscard]] int headerHeight() const;
|
||||
void appendItem(not_null<BaseLayout*> item);
|
||||
void setHeader(not_null<BaseLayout*> item);
|
||||
[[nodiscard]] bool belongsHere(not_null<BaseLayout*> item) const;
|
||||
[[nodiscard]] Items::iterator findItemAfterTop(int top);
|
||||
[[nodiscard]] Items::const_iterator findItemAfterTop(int top) const;
|
||||
[[nodiscard]] Items::const_iterator findItemAfterBottom(
|
||||
Items::const_iterator from,
|
||||
int bottom) const;
|
||||
[[nodiscard]] QRect findItemRect(not_null<const BaseLayout*> item) const;
|
||||
[[nodiscard]] ListFoundItem completeResult(
|
||||
not_null<BaseLayout*> item,
|
||||
bool exact) const;
|
||||
[[nodiscard]] TextSelection itemSelection(
|
||||
not_null<const BaseLayout*> item,
|
||||
const ListContext &context) const;
|
||||
|
||||
int recountHeight();
|
||||
void refreshHeight();
|
||||
|
||||
Type _type = Type{};
|
||||
bool _hasFloatingHeader = false;
|
||||
Ui::Text::String _header;
|
||||
Items _items;
|
||||
base::flat_map<
|
||||
not_null<const HistoryItem*>,
|
||||
not_null<BaseLayout*>> _byItem;
|
||||
int _itemsLeft = 0;
|
||||
int _itemsTop = 0;
|
||||
int _itemWidth = 0;
|
||||
int _itemsInRow = 1;
|
||||
mutable int _rowsCount = 0;
|
||||
int _top = 0;
|
||||
int _height = 0;
|
||||
|
||||
Mosaic::Layout::MosaicLayout<BaseLayout> _mosaic;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Info::Media
|
File diff suppressed because it is too large
Load diff
|
@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "info/media/info_media_widget.h"
|
||||
#include "data/data_shared_media.h"
|
||||
#include "info/media/info_media_common.h"
|
||||
#include "overview/overview_layout_delegate.h"
|
||||
|
||||
class DeleteMessagesBox;
|
||||
|
@ -45,8 +45,10 @@ class AbstractController;
|
|||
|
||||
namespace Media {
|
||||
|
||||
using BaseLayout = Overview::Layout::ItemBase;
|
||||
using UniversalMsgId = MsgId;
|
||||
struct ListFoundItem;
|
||||
struct ListContext;
|
||||
class ListSection;
|
||||
class ListProvider;
|
||||
|
||||
class ListWidget final
|
||||
: public Ui::RpWidget
|
||||
|
@ -89,12 +91,15 @@ public:
|
|||
bool showInMediaView = false) override;
|
||||
|
||||
private:
|
||||
struct Context;
|
||||
struct DateBadge;
|
||||
class Section;
|
||||
using Section = ListSection;
|
||||
using FoundItem = ListFoundItem;
|
||||
using CursorState = HistoryView::CursorState;
|
||||
using TextState = HistoryView::TextState;
|
||||
using StateRequest = HistoryView::StateRequest;
|
||||
using SelectionData = ListItemSelectionData;
|
||||
using SelectedMap = ListSelectedMap;
|
||||
using DragSelectAction = ListDragSelectAction;
|
||||
enum class MouseAction {
|
||||
None,
|
||||
PrepareDrag,
|
||||
|
@ -102,45 +107,14 @@ private:
|
|||
PrepareSelect,
|
||||
Selecting,
|
||||
};
|
||||
struct CachedItem {
|
||||
CachedItem(std::unique_ptr<BaseLayout> item);
|
||||
CachedItem(CachedItem &&other);
|
||||
CachedItem &operator=(CachedItem &&other);
|
||||
~CachedItem();
|
||||
|
||||
std::unique_ptr<BaseLayout> item;
|
||||
bool stale = false;
|
||||
};
|
||||
struct FoundItem {
|
||||
not_null<BaseLayout*> layout;
|
||||
QRect geometry;
|
||||
bool exact = false;
|
||||
};
|
||||
struct SelectionData {
|
||||
explicit SelectionData(TextSelection text) : text(text) {
|
||||
}
|
||||
|
||||
TextSelection text;
|
||||
bool canDelete = false;
|
||||
bool canForward = false;
|
||||
};
|
||||
using SelectedMap = base::flat_map<
|
||||
UniversalMsgId,
|
||||
SelectionData,
|
||||
std::less<>>;
|
||||
enum class DragSelectAction {
|
||||
None,
|
||||
Selecting,
|
||||
Deselecting,
|
||||
};
|
||||
struct MouseState {
|
||||
UniversalMsgId itemId = 0;
|
||||
HistoryItem *item = nullptr;
|
||||
QSize size;
|
||||
QPoint cursor;
|
||||
bool inside = false;
|
||||
|
||||
inline bool operator==(const MouseState &other) const {
|
||||
return (itemId == other.itemId)
|
||||
return (item == other.item)
|
||||
&& (cursor == other.cursor);
|
||||
}
|
||||
inline bool operator!=(const MouseState &other) const {
|
||||
|
@ -153,10 +127,6 @@ private:
|
|||
Touch,
|
||||
Other,
|
||||
};
|
||||
struct ScrollTopState {
|
||||
UniversalMsgId item = 0;
|
||||
int shift = 0;
|
||||
};
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void visibleTopBottomUpdated(
|
||||
|
@ -175,97 +145,83 @@ private:
|
|||
void start();
|
||||
int recountHeight();
|
||||
void refreshHeight();
|
||||
void subscribeToSession(not_null<Main::Session*> session);
|
||||
|
||||
void setupSelectRestriction();
|
||||
[[nodiscard]] bool hasSelectRestriction() const;
|
||||
|
||||
QMargins padding() const;
|
||||
bool isMyItem(not_null<const HistoryItem*> item) const;
|
||||
bool isItemLayout(
|
||||
not_null<const HistoryItem*> item,
|
||||
BaseLayout *layout) const;
|
||||
bool isPossiblyMyId(FullMsgId fullId) const;
|
||||
void repaintItem(const HistoryItem *item);
|
||||
void repaintItem(UniversalMsgId msgId);
|
||||
void repaintItem(const BaseLayout *item);
|
||||
void repaintItem(QRect itemGeometry);
|
||||
void itemRemoved(not_null<const HistoryItem*> item);
|
||||
void itemLayoutChanged(not_null<const HistoryItem*> item);
|
||||
|
||||
void refreshViewer();
|
||||
void invalidatePaletteCache();
|
||||
void refreshRows();
|
||||
SparseIdsMergedSlice::Key sliceKey(
|
||||
UniversalMsgId universalId) const;
|
||||
BaseLayout *getLayout(UniversalMsgId universalId);
|
||||
BaseLayout *getExistingLayout(UniversalMsgId universalId) const;
|
||||
std::unique_ptr<BaseLayout> createLayout(
|
||||
UniversalMsgId universalId,
|
||||
Type type);
|
||||
|
||||
SelectedItems collectSelectedItems() const;
|
||||
MessageIdsList collectSelectedIds() const;
|
||||
[[nodiscard]] SelectedItems collectSelectedItems() const;
|
||||
[[nodiscard]] MessageIdsList collectSelectedIds() const;
|
||||
void pushSelectedItems();
|
||||
FullMsgId computeFullId(UniversalMsgId universalId) const;
|
||||
bool hasSelected() const;
|
||||
bool isSelectedItem(
|
||||
[[nodiscard]] bool hasSelected() const;
|
||||
[[nodiscard]] bool isSelectedItem(
|
||||
const SelectedMap::const_iterator &i) const;
|
||||
void removeItemSelection(
|
||||
const SelectedMap::const_iterator &i);
|
||||
bool hasSelectedText() const;
|
||||
bool hasSelectedItems() const;
|
||||
[[nodiscard]] bool hasSelectedText() const;
|
||||
[[nodiscard]] bool hasSelectedItems() const;
|
||||
void clearSelected();
|
||||
void forwardSelected();
|
||||
void forwardItem(UniversalMsgId universalId);
|
||||
void forwardItem(GlobalMsgId globalId);
|
||||
void forwardItems(MessageIdsList &&items);
|
||||
void deleteSelected();
|
||||
void deleteItem(UniversalMsgId universalId);
|
||||
void deleteItem(GlobalMsgId globalId);
|
||||
DeleteMessagesBox *deleteItems(MessageIdsList &&items);
|
||||
void applyItemSelection(
|
||||
UniversalMsgId universalId,
|
||||
HistoryItem *item,
|
||||
TextSelection selection);
|
||||
void toggleItemSelection(
|
||||
UniversalMsgId universalId);
|
||||
SelectedMap::iterator itemUnderPressSelection();
|
||||
SelectedMap::const_iterator itemUnderPressSelection() const;
|
||||
void toggleItemSelection(not_null<HistoryItem*> item);
|
||||
[[nodiscard]] SelectedMap::iterator itemUnderPressSelection();
|
||||
[[nodiscard]] auto itemUnderPressSelection() const
|
||||
-> SelectedMap::const_iterator;
|
||||
bool isItemUnderPressSelected() const;
|
||||
bool requiredToStartDragging(not_null<BaseLayout*> layout) const;
|
||||
bool isPressInSelectedText(TextState state) const;
|
||||
[[nodiscard]] bool requiredToStartDragging(
|
||||
not_null<BaseLayout*> layout) const;
|
||||
[[nodiscard]] bool isPressInSelectedText(TextState state) const;
|
||||
void applyDragSelection();
|
||||
void applyDragSelection(SelectedMap &applyTo) const;
|
||||
bool changeItemSelection(
|
||||
SelectedMap &selected,
|
||||
UniversalMsgId universalId,
|
||||
TextSelection selection) const;
|
||||
|
||||
static bool IsAfter(
|
||||
[[nodiscard]] bool isAfter(
|
||||
const MouseState &a,
|
||||
const MouseState &b);
|
||||
static bool SkipSelectFromItem(const MouseState &state);
|
||||
static bool SkipSelectTillItem(const MouseState &state);
|
||||
const MouseState &b) const;
|
||||
[[nodiscard]] static bool SkipSelectFromItem(const MouseState &state);
|
||||
[[nodiscard]] static bool SkipSelectTillItem(const MouseState &state);
|
||||
|
||||
void markLayoutsStale();
|
||||
void clearStaleLayouts();
|
||||
std::vector<Section>::iterator findSectionByItem(
|
||||
UniversalMsgId universalId);
|
||||
std::vector<Section>::iterator findSectionAfterTop(int top);
|
||||
std::vector<Section>::const_iterator findSectionAfterTop(
|
||||
[[nodiscard]] std::vector<Section>::iterator findSectionByItem(
|
||||
not_null<const HistoryItem*> item);
|
||||
[[nodiscard]] std::vector<Section>::iterator findSectionAfterTop(
|
||||
int top);
|
||||
[[nodiscard]] std::vector<Section>::const_iterator findSectionAfterTop(
|
||||
int top) const;
|
||||
std::vector<Section>::const_iterator findSectionAfterBottom(
|
||||
[[nodiscard]] auto findSectionAfterBottom(
|
||||
std::vector<Section>::const_iterator from,
|
||||
int bottom) const;
|
||||
FoundItem findItemByPoint(QPoint point) const;
|
||||
std::optional<FoundItem> findItemById(UniversalMsgId universalId);
|
||||
FoundItem findItemDetails(not_null<BaseLayout*> item);
|
||||
FoundItem foundItemInSection(
|
||||
int bottom) const -> std::vector<Section>::const_iterator;
|
||||
[[nodiscard]] FoundItem findItemByPoint(QPoint point) const;
|
||||
[[nodiscard]] std::optional<FoundItem> findItemByItem(
|
||||
const HistoryItem *item);
|
||||
[[nodiscard]] FoundItem findItemDetails(not_null<BaseLayout*> item);
|
||||
[[nodiscard]] FoundItem foundItemInSection(
|
||||
const FoundItem &item,
|
||||
const Section §ion) const;
|
||||
|
||||
ScrollTopState countScrollState() const;
|
||||
[[nodiscard]] ListScrollTopState countScrollState() const;
|
||||
void saveScrollState();
|
||||
void restoreScrollState();
|
||||
|
||||
QPoint clampMousePosition(QPoint position) const;
|
||||
[[nodiscard]] QPoint clampMousePosition(QPoint position) const;
|
||||
void mouseActionStart(
|
||||
const QPoint &globalPosition,
|
||||
Qt::MouseButton button);
|
||||
|
@ -276,7 +232,7 @@ private:
|
|||
Qt::MouseButton button);
|
||||
void mouseActionCancel();
|
||||
void performDrag();
|
||||
style::cursor computeMouseCursor() const;
|
||||
[[nodiscard]] style::cursor computeMouseCursor() const;
|
||||
void showContextMenu(
|
||||
QContextMenuEvent *e,
|
||||
ContextMenuSource source);
|
||||
|
@ -298,24 +254,15 @@ private:
|
|||
void setActionBoxWeak(QPointer<Ui::RpWidget> box);
|
||||
|
||||
const not_null<AbstractController*> _controller;
|
||||
const not_null<PeerData*> _peer;
|
||||
PeerData * const _migrated = nullptr;
|
||||
const Type _type = Type::Photo;
|
||||
const std::unique_ptr<ListProvider> _provider;
|
||||
|
||||
static constexpr auto kMinimalIdsLimit = 16;
|
||||
static constexpr auto kDefaultAroundId = (ServerMaxMsgId - 1);
|
||||
UniversalMsgId _universalAroundId = kDefaultAroundId;
|
||||
int _idsLimit = kMinimalIdsLimit;
|
||||
SparseIdsMergedSlice _slice;
|
||||
|
||||
std::unordered_map<UniversalMsgId, CachedItem> _layouts;
|
||||
base::flat_set<not_null<const BaseLayout*>> _heavyLayouts;
|
||||
bool _heavyLayoutsInvalidated = false;
|
||||
std::vector<Section> _sections;
|
||||
|
||||
int _visibleTop = 0;
|
||||
int _visibleBottom = 0;
|
||||
ScrollTopState _scrollTopState;
|
||||
ListScrollTopState _scrollTopState;
|
||||
rpl::event_stream<int> _scrollToRequests;
|
||||
|
||||
MouseAction _mouseAction = MouseAction::None;
|
||||
|
@ -324,7 +271,7 @@ private:
|
|||
MouseState _overState;
|
||||
MouseState _pressState;
|
||||
BaseLayout *_overLayout = nullptr;
|
||||
UniversalMsgId _contextUniversalId = 0;
|
||||
HistoryItem *_contextItem = nullptr;
|
||||
CursorState _mouseCursorState = CursorState();
|
||||
uint16 _mouseTextSymbol = 0;
|
||||
bool _pressWasInactive = false;
|
||||
|
@ -345,8 +292,6 @@ private:
|
|||
QPoint _trippleClickPoint;
|
||||
crl::time _trippleClickStartTime = 0;
|
||||
|
||||
rpl::lifetime _viewerLifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media
|
||||
|
|
450
Telegram/SourceFiles/info/media/info_media_provider.cpp
Normal file
450
Telegram/SourceFiles/info/media/info_media_provider.cpp
Normal file
|
@ -0,0 +1,450 @@
|
|||
/*
|
||||
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 "info/media/info_media_provider.h"
|
||||
|
||||
#include "info/media/info_media_widget.h"
|
||||
#include "info/media/info_media_list_section.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "layout/layout_selection.h"
|
||||
#include "main/main_session.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Info::Media {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPreloadedScreensCount = 4;
|
||||
constexpr auto kPreloadedScreensCountFull
|
||||
= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
|
||||
|
||||
[[nodiscard]] int MinItemHeight(Type type, int width) {
|
||||
auto &songSt = st::overviewFileLayout;
|
||||
|
||||
switch (type) {
|
||||
case Type::Photo:
|
||||
case Type::GIF:
|
||||
case Type::Video:
|
||||
case Type::RoundFile: {
|
||||
auto itemsLeft = st::infoMediaSkip;
|
||||
auto itemsInRow = (width - itemsLeft)
|
||||
/ (st::infoMediaMinGridSize + st::infoMediaSkip);
|
||||
return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow;
|
||||
} break;
|
||||
|
||||
case Type::RoundVoiceFile:
|
||||
return songSt.songPadding.top() + songSt.songThumbSize + songSt.songPadding.bottom() + st::lineWidth;
|
||||
case Type::File:
|
||||
return songSt.filePadding.top() + songSt.fileThumbSize + songSt.filePadding.bottom() + st::lineWidth;
|
||||
case Type::MusicFile:
|
||||
return songSt.songPadding.top() + songSt.songThumbSize + songSt.songPadding.bottom();
|
||||
case Type::Link:
|
||||
return st::linksPhotoSize + st::linksMargin.top() + st::linksMargin.bottom() + st::linksBorder;
|
||||
}
|
||||
Unexpected("Type in MinItemHeight()");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Provider::Provider(not_null<AbstractController*> controller)
|
||||
: _controller(controller)
|
||||
, _peer(_controller->key().peer())
|
||||
, _migrated(_controller->migrated())
|
||||
, _type(_controller->section().mediaType())
|
||||
, _slice(sliceKey(_universalAroundId)) {
|
||||
_controller->session().data().itemRemoved(
|
||||
) | rpl::start_with_next([this](auto item) {
|
||||
itemRemoved(item);
|
||||
}, _lifetime);
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
for (auto &layout : _layouts) {
|
||||
layout.second.item->invalidateCache();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
Type Provider::type() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
bool Provider::hasSelectRestriction() {
|
||||
if (_peer->allowsForwarding()) {
|
||||
return false;
|
||||
} else if (const auto chat = _peer->asChat()) {
|
||||
return !chat->canDeleteMessages();
|
||||
} else if (const auto channel = _peer->asChannel()) {
|
||||
return !channel->canDeleteMessages();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
|
||||
if (_peer->isUser()) {
|
||||
return rpl::never<bool>();
|
||||
}
|
||||
const auto chat = _peer->asChat();
|
||||
const auto channel = _peer->asChannel();
|
||||
auto noForwards = chat
|
||||
? Data::PeerFlagValue(chat, ChatDataFlag::NoForwards)
|
||||
: Data::PeerFlagValue(
|
||||
channel,
|
||||
ChannelDataFlag::NoForwards
|
||||
) | rpl::type_erased();
|
||||
|
||||
auto rights = chat
|
||||
? chat->adminRightsValue()
|
||||
: channel->adminRightsValue();
|
||||
auto canDelete = std::move(
|
||||
rights
|
||||
) | rpl::map([=] {
|
||||
return chat
|
||||
? chat->canDeleteMessages()
|
||||
: channel->canDeleteMessages();
|
||||
});
|
||||
return rpl::combine(
|
||||
std::move(noForwards),
|
||||
std::move(canDelete)
|
||||
) | rpl::map([=] {
|
||||
return hasSelectRestriction();
|
||||
}) | rpl::distinct_until_changed() | rpl::skip(1);
|
||||
}
|
||||
|
||||
bool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {
|
||||
return isPossiblyMyPeerId(item->history()->peer->id);
|
||||
}
|
||||
|
||||
bool Provider::isPossiblyMyPeerId(PeerId peerId) const {
|
||||
return (peerId == _peer->id) || (_migrated && peerId == _migrated->id);
|
||||
}
|
||||
|
||||
std::optional<int> Provider::fullCount() {
|
||||
return _slice.fullCount();
|
||||
}
|
||||
|
||||
void Provider::restart() {
|
||||
_layouts.clear();
|
||||
_universalAroundId = kDefaultAroundId;
|
||||
_idsLimit = kMinimalIdsLimit;
|
||||
_slice = SparseIdsMergedSlice(sliceKey(_universalAroundId));
|
||||
refreshViewer();
|
||||
}
|
||||
|
||||
void Provider::checkPreload(
|
||||
QSize viewport,
|
||||
not_null<BaseLayout*> topLayout,
|
||||
not_null<BaseLayout*> bottomLayout,
|
||||
bool preloadTop,
|
||||
bool preloadBottom) {
|
||||
const auto visibleWidth = viewport.width();
|
||||
const auto visibleHeight = viewport.height();
|
||||
const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
|
||||
const auto minItemHeight = MinItemHeight(_type, visibleWidth);
|
||||
const auto preloadedCount = preloadedHeight / minItemHeight;
|
||||
const auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
|
||||
const auto preloadIdsLimit = preloadIdsLimitMin
|
||||
+ (visibleHeight / minItemHeight);
|
||||
const auto after = _slice.skippedAfter();
|
||||
const auto topLoaded = after && (*after == 0);
|
||||
const auto before = _slice.skippedBefore();
|
||||
const auto bottomLoaded = before && (*before == 0);
|
||||
|
||||
const auto minScreenDelta = kPreloadedScreensCount
|
||||
- kPreloadIfLessThanScreens;
|
||||
const auto minUniversalIdDelta = (minScreenDelta * visibleHeight)
|
||||
/ minItemHeight;
|
||||
const auto preloadAroundItem = [&](not_null<BaseLayout*> layout) {
|
||||
auto preloadRequired = false;
|
||||
auto universalId = GetUniversalId(layout);
|
||||
if (!preloadRequired) {
|
||||
preloadRequired = (_idsLimit < preloadIdsLimitMin);
|
||||
}
|
||||
if (!preloadRequired) {
|
||||
auto delta = _slice.distance(
|
||||
sliceKey(_universalAroundId),
|
||||
sliceKey(universalId));
|
||||
Assert(delta != std::nullopt);
|
||||
preloadRequired = (qAbs(*delta) >= minUniversalIdDelta);
|
||||
}
|
||||
if (preloadRequired) {
|
||||
_idsLimit = preloadIdsLimit;
|
||||
_universalAroundId = universalId;
|
||||
refreshViewer();
|
||||
}
|
||||
};
|
||||
|
||||
if (preloadTop && !topLoaded) {
|
||||
preloadAroundItem(topLayout);
|
||||
} else if (preloadBottom && !bottomLoaded) {
|
||||
preloadAroundItem(bottomLayout);
|
||||
}
|
||||
}
|
||||
|
||||
void Provider::refreshViewer() {
|
||||
_viewerLifetime.destroy();
|
||||
const auto idForViewer = sliceKey(_universalAroundId).universalId;
|
||||
_controller->mediaSource(
|
||||
idForViewer,
|
||||
_idsLimit,
|
||||
_idsLimit
|
||||
) | rpl::start_with_next([=](SparseIdsMergedSlice &&slice) {
|
||||
if (!slice.fullCount()) {
|
||||
// Don't display anything while full count is unknown.
|
||||
return;
|
||||
}
|
||||
_slice = std::move(slice);
|
||||
if (auto nearest = _slice.nearest(idForViewer)) {
|
||||
_universalAroundId = GetUniversalId(*nearest);
|
||||
}
|
||||
_refreshed.fire({});
|
||||
}, _viewerLifetime);
|
||||
}
|
||||
|
||||
rpl::producer<> Provider::refreshed() {
|
||||
return _refreshed.events();
|
||||
}
|
||||
|
||||
std::vector<ListSection> Provider::fillSections(
|
||||
not_null<Overview::Layout::Delegate*> delegate) {
|
||||
markLayoutsStale();
|
||||
const auto guard = gsl::finally([&] { clearStaleLayouts(); });
|
||||
|
||||
auto result = std::vector<ListSection>();
|
||||
auto section = ListSection(_type);
|
||||
auto count = _slice.size();
|
||||
for (auto i = count; i != 0;) {
|
||||
auto universalId = GetUniversalId(_slice[--i]);
|
||||
if (auto layout = getLayout(universalId, delegate)) {
|
||||
if (!section.addItem(layout)) {
|
||||
section.finishSection();
|
||||
result.push_back(std::move(section));
|
||||
section = ListSection(_type);
|
||||
section.addItem(layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!section.empty()) {
|
||||
section.finishSection();
|
||||
result.push_back(std::move(section));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Provider::markLayoutsStale() {
|
||||
for (auto &layout : _layouts) {
|
||||
layout.second.stale = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Provider::clearStaleLayouts() {
|
||||
for (auto i = _layouts.begin(); i != _layouts.end();) {
|
||||
if (i->second.stale) {
|
||||
_layoutRemoved.fire(i->second.item.get());
|
||||
i = _layouts.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<not_null<BaseLayout*>> Provider::layoutRemoved() {
|
||||
return _layoutRemoved.events();
|
||||
}
|
||||
|
||||
BaseLayout *Provider::lookupLayout(
|
||||
const HistoryItem *item) {
|
||||
const auto i = _layouts.find(GetUniversalId(item));
|
||||
return (i != _layouts.end()) ? i->second.item.get() : nullptr;
|
||||
}
|
||||
|
||||
bool Provider::isMyItem(not_null<const HistoryItem*> item) {
|
||||
const auto peer = item->history()->peer;
|
||||
return (_peer == peer) || (_migrated == peer);
|
||||
}
|
||||
|
||||
bool Provider::isAfter(
|
||||
not_null<const HistoryItem*> a,
|
||||
not_null<const HistoryItem*> b) {
|
||||
return (GetUniversalId(a) < GetUniversalId(b));
|
||||
}
|
||||
|
||||
SparseIdsMergedSlice::Key Provider::sliceKey(
|
||||
UniversalMsgId universalId) const {
|
||||
using Key = SparseIdsMergedSlice::Key;
|
||||
if (_migrated) {
|
||||
return Key(_peer->id, _migrated->id, universalId);
|
||||
}
|
||||
if (universalId < 0) {
|
||||
// Convert back to plain id for non-migrated histories.
|
||||
universalId = universalId + ServerMaxMsgId;
|
||||
}
|
||||
return Key(_peer->id, 0, universalId);
|
||||
}
|
||||
|
||||
void Provider::itemRemoved(not_null<const HistoryItem*> item) {
|
||||
const auto id = GetUniversalId(item);
|
||||
if (const auto i = _layouts.find(id); i != end(_layouts)) {
|
||||
_layoutRemoved.fire(i->second.item.get());
|
||||
_layouts.erase(i);
|
||||
}
|
||||
}
|
||||
|
||||
FullMsgId Provider::computeFullId(
|
||||
UniversalMsgId universalId) const {
|
||||
Expects(universalId != 0);
|
||||
|
||||
return (universalId > 0)
|
||||
? FullMsgId(_peer->id, universalId)
|
||||
: FullMsgId(
|
||||
(_migrated ? _migrated : _peer.get())->id,
|
||||
ServerMaxMsgId + universalId);
|
||||
}
|
||||
|
||||
BaseLayout *Provider::getLayout(
|
||||
UniversalMsgId universalId,
|
||||
not_null<Overview::Layout::Delegate*> delegate) {
|
||||
auto it = _layouts.find(universalId);
|
||||
if (it == _layouts.end()) {
|
||||
if (auto layout = createLayout(universalId, delegate, _type)) {
|
||||
layout->initDimensions();
|
||||
it = _layouts.emplace(
|
||||
universalId,
|
||||
std::move(layout)).first;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
it->second.stale = false;
|
||||
return it->second.item.get();
|
||||
}
|
||||
|
||||
std::unique_ptr<BaseLayout> Provider::createLayout(
|
||||
UniversalMsgId universalId,
|
||||
not_null<Overview::Layout::Delegate*> delegate,
|
||||
Type type) {
|
||||
const auto item = _controller->session().data().message(
|
||||
computeFullId(universalId));
|
||||
if (!item) {
|
||||
return nullptr;
|
||||
}
|
||||
auto getPhoto = [&]() -> PhotoData* {
|
||||
if (const auto media = item->media()) {
|
||||
return media->photo();
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
auto getFile = [&]() -> DocumentData* {
|
||||
if (auto media = item->media()) {
|
||||
return media->document();
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
auto &songSt = st::overviewFileLayout;
|
||||
using namespace Overview::Layout;
|
||||
switch (type) {
|
||||
case Type::Photo:
|
||||
if (const auto photo = getPhoto()) {
|
||||
return std::make_unique<Photo>(delegate, item, photo);
|
||||
}
|
||||
return nullptr;
|
||||
case Type::GIF:
|
||||
if (const auto file = getFile()) {
|
||||
return std::make_unique<Gif>(delegate, item, file);
|
||||
}
|
||||
return nullptr;
|
||||
case Type::Video:
|
||||
if (const auto file = getFile()) {
|
||||
return std::make_unique<Video>(delegate, item, file);
|
||||
}
|
||||
return nullptr;
|
||||
case Type::File:
|
||||
if (const auto file = getFile()) {
|
||||
return std::make_unique<Document>(delegate, item, file, songSt);
|
||||
}
|
||||
return nullptr;
|
||||
case Type::MusicFile:
|
||||
if (const auto file = getFile()) {
|
||||
return std::make_unique<Document>(delegate, item, file, songSt);
|
||||
}
|
||||
return nullptr;
|
||||
case Type::RoundVoiceFile:
|
||||
if (const auto file = getFile()) {
|
||||
return std::make_unique<Voice>(delegate, item, file, songSt);
|
||||
}
|
||||
return nullptr;
|
||||
case Type::Link:
|
||||
return std::make_unique<Link>(delegate, item, item->media());
|
||||
case Type::RoundFile:
|
||||
return nullptr;
|
||||
}
|
||||
Unexpected("Type in ListWidget::createLayout()");
|
||||
}
|
||||
|
||||
void Provider::applyDragSelection(
|
||||
ListSelectedMap &selected,
|
||||
not_null<const HistoryItem*> fromItem,
|
||||
bool skipFrom,
|
||||
not_null<const HistoryItem*> tillItem,
|
||||
bool skipTill) {
|
||||
const auto fromId = GetUniversalId(fromItem) - (skipFrom ? 1 : 0);
|
||||
const auto tillId = GetUniversalId(tillItem) - (skipTill ? 0 : 1);
|
||||
for (auto i = selected.begin(); i != selected.end();) {
|
||||
const auto itemId = GetUniversalId(i->first);
|
||||
if (itemId > fromId || itemId <= tillId) {
|
||||
i = selected.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
for (auto &layoutItem : _layouts) {
|
||||
auto &&universalId = layoutItem.first;
|
||||
if (universalId <= fromId && universalId > tillId) {
|
||||
ChangeItemSelection(
|
||||
selected,
|
||||
layoutItem.second.item->getItem(),
|
||||
FullSelection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Provider::saveState(
|
||||
not_null<Memento*> memento,
|
||||
ListScrollTopState scrollState) {
|
||||
if (_universalAroundId != kDefaultAroundId && scrollState.item) {
|
||||
memento->setAroundId(computeFullId(_universalAroundId));
|
||||
memento->setIdsLimit(_idsLimit);
|
||||
memento->setScrollTopItem(scrollState.item->globalId());
|
||||
memento->setScrollTopShift(scrollState.shift);
|
||||
}
|
||||
}
|
||||
|
||||
void Provider::restoreState(
|
||||
not_null<Memento*> memento,
|
||||
Fn<void(ListScrollTopState)> restoreScrollState) {
|
||||
if (const auto limit = memento->idsLimit()) {
|
||||
auto wasAroundId = memento->aroundId();
|
||||
if (isPossiblyMyPeerId(wasAroundId.peer)) {
|
||||
_idsLimit = limit;
|
||||
_universalAroundId = GetUniversalId(wasAroundId);
|
||||
restoreScrollState({
|
||||
.item = MessageByGlobalId(memento->scrollTopItem()),
|
||||
.shift = memento->scrollTopShift(),
|
||||
});
|
||||
refreshViewer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Info::Media
|
103
Telegram/SourceFiles/info/media/info_media_provider.h
Normal file
103
Telegram/SourceFiles/info/media/info_media_provider.h
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
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 "info/media/info_media_common.h"
|
||||
#include "data/data_shared_media.h"
|
||||
|
||||
namespace Info {
|
||||
class AbstractController;
|
||||
} // namespace Info
|
||||
|
||||
namespace Info::Media {
|
||||
|
||||
class Provider final : public ListProvider {
|
||||
public:
|
||||
explicit Provider(not_null<AbstractController*> controller);
|
||||
|
||||
Type type() override;
|
||||
bool hasSelectRestriction() override;
|
||||
rpl::producer<bool> hasSelectRestrictionChanges() override;
|
||||
bool isPossiblyMyItem(not_null<const HistoryItem*> item) override;
|
||||
|
||||
std::optional<int> fullCount() override;
|
||||
|
||||
void restart() override;
|
||||
void checkPreload(
|
||||
QSize viewport,
|
||||
not_null<BaseLayout*> topLayout,
|
||||
not_null<BaseLayout*> bottomLayout,
|
||||
bool preloadTop,
|
||||
bool preloadBottom) override;
|
||||
void refreshViewer() override;
|
||||
rpl::producer<> refreshed() override;
|
||||
|
||||
std::vector<ListSection> fillSections(
|
||||
not_null<Overview::Layout::Delegate*> delegate) override;
|
||||
rpl::producer<not_null<BaseLayout*>> layoutRemoved() override;
|
||||
BaseLayout *lookupLayout(const HistoryItem *item) override;
|
||||
bool isMyItem(not_null<const HistoryItem*> item) override;
|
||||
bool isAfter(
|
||||
not_null<const HistoryItem*> a,
|
||||
not_null<const HistoryItem*> b) override;
|
||||
|
||||
void applyDragSelection(
|
||||
ListSelectedMap &selected,
|
||||
not_null<const HistoryItem*> fromItem,
|
||||
bool skipFrom,
|
||||
not_null<const HistoryItem*> tillItem,
|
||||
bool skipTill) override;
|
||||
|
||||
void saveState(
|
||||
not_null<Memento*> memento,
|
||||
ListScrollTopState scrollState) override;
|
||||
void restoreState(
|
||||
not_null<Memento*> memento,
|
||||
Fn<void(ListScrollTopState)> restoreScrollState) override;
|
||||
|
||||
private:
|
||||
static constexpr auto kMinimalIdsLimit = 16;
|
||||
static constexpr auto kDefaultAroundId = (ServerMaxMsgId - 1);
|
||||
|
||||
[[nodiscard]] bool isPossiblyMyPeerId(PeerId peerId) const;
|
||||
[[nodiscard]] FullMsgId computeFullId(UniversalMsgId universalId) const;
|
||||
[[nodiscard]] BaseLayout *getLayout(
|
||||
UniversalMsgId universalId,
|
||||
not_null<Overview::Layout::Delegate*> delegate);
|
||||
[[nodiscard]] std::unique_ptr<BaseLayout> createLayout(
|
||||
UniversalMsgId universalId,
|
||||
not_null<Overview::Layout::Delegate*> delegate,
|
||||
Type type);
|
||||
|
||||
[[nodiscard]] SparseIdsMergedSlice::Key sliceKey(
|
||||
UniversalMsgId universalId) const;
|
||||
|
||||
void itemRemoved(not_null<const HistoryItem*> item);
|
||||
void markLayoutsStale();
|
||||
void clearStaleLayouts();
|
||||
|
||||
const not_null<AbstractController*> _controller;
|
||||
|
||||
const not_null<PeerData*> _peer;
|
||||
PeerData * const _migrated = nullptr;
|
||||
const Type _type = Type::Photo;
|
||||
|
||||
UniversalMsgId _universalAroundId = kDefaultAroundId;
|
||||
int _idsLimit = kMinimalIdsLimit;
|
||||
SparseIdsMergedSlice _slice;
|
||||
|
||||
std::unordered_map<UniversalMsgId, CachedItem> _layouts;
|
||||
rpl::event_stream<not_null<BaseLayout*>> _layoutRemoved;
|
||||
rpl::event_stream<> _refreshed;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
rpl::lifetime _viewerLifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Info::Media
|
|
@ -9,10 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
#include "info/media/info_media_inner_widget.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/search_field_controller.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_user.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Info {
|
||||
|
@ -38,9 +40,13 @@ Type TabIndexToType(int index) {
|
|||
|
||||
Memento::Memento(not_null<Controller*> controller)
|
||||
: Memento(
|
||||
controller->peer(),
|
||||
(controller->peer()
|
||||
? controller->peer()
|
||||
: controller->parentController()->session().user()),
|
||||
controller->migratedPeerId(),
|
||||
controller->section().mediaType()) {
|
||||
(controller->section().type() == Section::Type::Downloads
|
||||
? Type::File
|
||||
: controller->section().mediaType())) {
|
||||
}
|
||||
|
||||
Memento::Memento(not_null<PeerData*> peer, PeerId migratedPeerId, Type type)
|
||||
|
|
|
@ -40,6 +40,7 @@ public:
|
|||
return _type;
|
||||
}
|
||||
|
||||
// Only for media, not for downloads.
|
||||
void setAroundId(FullMsgId aroundId) {
|
||||
_aroundId = aroundId;
|
||||
}
|
||||
|
@ -52,10 +53,11 @@ public:
|
|||
int idsLimit() const {
|
||||
return _idsLimit;
|
||||
}
|
||||
void setScrollTopItem(FullMsgId item) {
|
||||
|
||||
void setScrollTopItem(GlobalMsgId item) {
|
||||
_scrollTopItem = item;
|
||||
}
|
||||
FullMsgId scrollTopItem() const {
|
||||
GlobalMsgId scrollTopItem() const {
|
||||
return _scrollTopItem;
|
||||
}
|
||||
void setScrollTopShift(int shift) {
|
||||
|
@ -75,7 +77,7 @@ private:
|
|||
Type _type = Type::Photo;
|
||||
FullMsgId _aroundId;
|
||||
int _idsLimit = 0;
|
||||
FullMsgId _scrollTopItem;
|
||||
GlobalMsgId _scrollTopItem;
|
||||
int _scrollTopShift = 0;
|
||||
SearchState _searchState;
|
||||
|
||||
|
|
|
@ -301,7 +301,7 @@ void Panel::refreshList() {
|
|||
section().mediaType());
|
||||
memento.setAroundId(contextId);
|
||||
memento.setIdsLimit(kPlaylistIdsLimit);
|
||||
memento.setScrollTopItem(contextId);
|
||||
memento.setScrollTopItem({ contextId, peer->session().uniqueId() });
|
||||
memento.setScrollTopShift(-st::infoMediaMargin.top());
|
||||
weak->restoreState(&memento);
|
||||
}
|
||||
|
|
|
@ -53,9 +53,9 @@ public:
|
|||
TextSelection selection,
|
||||
const PaintContext *context) = 0;
|
||||
|
||||
QDateTime dateTime() const;
|
||||
[[nodiscard]] QDateTime dateTime() const;
|
||||
|
||||
HistoryItem *getItem() const {
|
||||
[[nodiscard]] not_null<HistoryItem*> getItem() const {
|
||||
return _parent;
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ protected:
|
|||
QPoint position,
|
||||
bool selected,
|
||||
const PaintContext *context);
|
||||
virtual const style::RoundCheckbox &checkboxStyle() const;
|
||||
[[nodiscard]] virtual const style::RoundCheckbox &checkboxStyle() const;
|
||||
|
||||
private:
|
||||
void ensureCheckboxCreated();
|
||||
|
|
|
@ -41,6 +41,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "window/window_adaptive.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "info/downloads/info_downloads_widget.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "storage/localstorage.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "core/application.h"
|
||||
|
@ -900,6 +902,8 @@ void SetupDataStorage(
|
|||
st::settingsButton,
|
||||
{ &st::settingsIconDownload, kIconPurple }
|
||||
)->setClickedCallback([=] {
|
||||
controller->showSection(
|
||||
Info::Downloads::Make(controller->session().user()));
|
||||
});
|
||||
|
||||
const auto ask = AddButton(
|
||||
|
|
Loading…
Add table
Reference in a new issue