From b8e10fb34b1b7273810f33019d3e5c09b3441dc1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 12 Aug 2025 19:40:39 +0400 Subject: [PATCH] Proof-of-concept saved music API support. --- Telegram/CMakeLists.txt | 7 + .../SourceFiles/data/data_saved_music.cpp | 269 ++++++++++ Telegram/SourceFiles/data/data_saved_music.h | 65 +++ Telegram/SourceFiles/data/data_session.cpp | 2 + Telegram/SourceFiles/data/data_session.h | 5 + Telegram/SourceFiles/data/data_stories.cpp | 2 +- .../history/history_inner_widget.cpp | 15 + .../SourceFiles/info/info_content_widget.cpp | 7 + .../SourceFiles/info/info_content_widget.h | 9 + Telegram/SourceFiles/info/info_controller.cpp | 11 + Telegram/SourceFiles/info/info_controller.h | 8 + .../info/media/info_media_buttons.cpp | 41 ++ .../info/media/info_media_buttons.h | 5 + .../info/media/info_media_list_widget.cpp | 6 + .../profile/info_profile_inner_widget.cpp | 9 + .../info/saved/info_saved_music_common.h | 20 + .../info/saved/info_saved_music_provider.cpp | 462 ++++++++++++++++++ .../info/saved/info_saved_music_provider.h | 141 ++++++ .../info/saved/info_saved_music_widget.cpp | 339 +++++++++++++ .../info/saved/info_saved_music_widget.h | 81 +++ .../stories/info_stories_inner_widget.cpp | 10 + .../info/stories/info_stories_provider.cpp | 1 - 22 files changed, 1513 insertions(+), 2 deletions(-) create mode 100644 Telegram/SourceFiles/data/data_saved_music.cpp create mode 100644 Telegram/SourceFiles/data/data_saved_music.h create mode 100644 Telegram/SourceFiles/info/saved/info_saved_music_common.h create mode 100644 Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp create mode 100644 Telegram/SourceFiles/info/saved/info_saved_music_provider.h create mode 100644 Telegram/SourceFiles/info/saved/info_saved_music_widget.cpp create mode 100644 Telegram/SourceFiles/info/saved/info_saved_music_widget.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 76ea20879b..ed133b857f 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -636,6 +636,8 @@ PRIVATE data/data_report.h data/data_saved_messages.cpp data/data_saved_messages.h + data/data_saved_music.cpp + data/data_saved_music.h data/data_saved_sublist.cpp data/data_saved_sublist.h data/data_search_controller.cpp @@ -1050,6 +1052,11 @@ PRIVATE info/reactions_list/info_reactions_list_widget.h info/requests_list/info_requests_list_widget.cpp info/requests_list/info_requests_list_widget.h + info/saved/info_saved_music_common.h + info/saved/info_saved_music_provider.cpp + info/saved/info_saved_music_provider.h + info/saved/info_saved_music_widget.cpp + info/saved/info_saved_music_widget.h info/saved/info_saved_sublists_widget.cpp info/saved/info_saved_sublists_widget.h info/settings/info_settings_widget.cpp diff --git a/Telegram/SourceFiles/data/data_saved_music.cpp b/Telegram/SourceFiles/data/data_saved_music.cpp new file mode 100644 index 0000000000..5b1e07d43e --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_music.cpp @@ -0,0 +1,269 @@ +/* +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 "data/data_saved_music.h" + +#include "api/api_hash.h" +#include "apiwrap.h" +#include "data/data_document.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "main/main_session.h" +#include "ui/ui_utility.h" + +namespace Data { +namespace { + +constexpr auto kPerPage = 50; + +} // namespace + +SavedMusic::SavedMusic(not_null owner) +: _owner(owner) { +} + +bool SavedMusic::Supported(PeerId peerId) { + return peerId && peerIsUser(peerId); +} + +bool SavedMusic::has(not_null document) const { + const auto entry = lookupEntry(_owner->session().userPeerId()); + return entry && ranges::contains(entry->list, document); +} + +void SavedMusic::save(not_null document) { + const auto peerId = _owner->session().userPeerId(); + auto &entry = _entries[peerId]; + if (entry.list.empty() && !entry.loaded) { + loadMore(peerId); + } + if (ranges::contains(entry.list, document)) { + return; + } + entry.list.insert(begin(entry.list), document); + if (entry.total >= 0) { + ++entry.total; + } + _owner->session().api().request(MTPaccount_SaveMusic( + MTP_flags(0), + document->mtpInput(), + MTPInputDocument() + )).send(); + _changed.fire_copy(peerId); +} + +void SavedMusic::remove(not_null document) { + const auto peerId = _owner->session().userPeerId(); + auto &entry = _entries[peerId]; + const auto i = ranges::remove(entry.list, document); + if (const auto removed = int(end(entry.list) - i)) { + entry.list.erase(i, end(entry.list)); + if (entry.total >= 0) { + entry.total = std::max(entry.total - removed, 0); + } + } + _owner->session().api().request(MTPaccount_SaveMusic( + MTP_flags(MTPaccount_SaveMusic::Flag::f_unsave), + document->mtpInput(), + MTPInputDocument() + )).send(); + _changed.fire_copy(peerId); +} + +bool SavedMusic::countKnown(PeerId peerId) const { + if (!Supported(peerId)) { + return true; + } + const auto entry = lookupEntry(peerId); + return entry && entry->total >= 0; +} + +int SavedMusic::count(PeerId peerId) const { + if (!Supported(peerId)) { + return 0; + } + const auto entry = lookupEntry(peerId); + return entry ? std::max(entry->total, 0) : 0; +} + +const std::vector> &SavedMusic::list( + PeerId peerId) const { + static const auto empty = std::vector>(); + if (!Supported(peerId)) { + return empty; + } + + const auto entry = lookupEntry(peerId); + return entry ? entry->list : empty; +} + +void SavedMusic::loadMore(PeerId peerId) { + loadMore(peerId, false); +} + +void SavedMusic::loadMore(PeerId peerId, bool reload) { + if (!Supported(peerId)) { + return; + } + + auto &entry = _entries[peerId]; + if (!entry.reloading && reload) { + _owner->session().api().request( + base::take(entry.requestId)).cancel(); + } + if ((!reload && entry.loaded) || entry.requestId) { + return; + } + const auto user = _owner->peer(peerId)->asUser(); + Assert(user != nullptr); + + entry.reloading = reload; + entry.requestId = _owner->session().api().request(MTPusers_GetSavedMusic( + user->inputUser, + MTP_int(reload ? 0 : entry.list.size()), + MTP_int(kPerPage), + MTP_long(reload ? firstPageHash(entry) : 0) + )).done([=](const MTPusers_SavedMusic &result) { + auto &entry = _entries[peerId]; + entry.requestId = 0; + const auto reloaded = base::take(entry.reloading); + result.match([&](const MTPDusers_savedMusicNotModified &) { + }, [&](const MTPDusers_savedMusic &data) { + const auto list = data.vdocuments().v; + const auto count = int(list.size()); + entry.total = std::max(count, data.vcount().v); + if (reloaded) { + entry.list.clear(); + } + for (const auto &item : list) { + const auto document = _owner->processDocument(item); + if (!ranges::contains(entry.list, document)) { + entry.list.push_back(document); + } + } + entry.loaded = list.empty() || (count == entry.list.size()); + }); + _changed.fire_copy(peerId); + }).fail([=](const MTP::Error &error) { + auto &entry = _entries[peerId]; + entry.requestId = 0; + entry.total = int(entry.list.size()); + entry.loaded = true; + _changed.fire_copy(peerId); + }).send(); +} + +uint64 SavedMusic::firstPageHash(const Entry &entry) const { + return Api::CountHash(entry.list + | ranges::views::transform(&DocumentData::id) + | ranges::views::take(kPerPage)); +} + +rpl::producer SavedMusic::changed() const { + return _changed.events(); +} + +SavedMusic::Entry *SavedMusic::lookupEntry(PeerId peerId) { + if (!Supported(peerId)) { + return nullptr; + } + + auto it = _entries.find(peerId); + if (it == end(_entries)) { + return nullptr; + } + return &it->second; +} + +const SavedMusic::Entry *SavedMusic::lookupEntry(PeerId peerId) const { + return const_cast(this)->lookupEntry(peerId); +} + +rpl::producer SavedMusicList( + not_null peer, + DocumentData *aroundId, + int limit) { + if (!peer->isUser()) { + return rpl::single(SavedMusicSlice({}, 0, 0, 0)); + } + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + SavedMusicSlice slice; + base::has_weak_ptr guard; + bool scheduled = false; + }; + const auto state = lifetime.make_state(); + + const auto push = [=] { + state->scheduled = false; + + const auto peerId = peer->id; + const auto savedMusic = &peer->owner().savedMusic(); + if (!savedMusic->countKnown(peerId)) { + return; + } + const auto &loaded = savedMusic->list(peerId); + const auto count = savedMusic->count(peerId); + auto i = aroundId + ? ranges::find(loaded, not_null(aroundId)) + : begin(loaded); + if (i == end(loaded)) { + i = begin(loaded); + } + const auto hasBefore = int(i - begin(loaded)); + const auto hasAfter = int(end(loaded) - i); + if (hasAfter < limit) { + savedMusic->loadMore(peerId); + } + const auto takeBefore = std::min(hasBefore, limit); + const auto takeAfter = std::min(hasAfter, limit); + auto ids = std::vector>(); + ids.reserve(takeBefore + takeAfter); + for (auto j = i - takeBefore; j != i + takeAfter; ++j) { + ids.push_back(*j); + } + const auto added = int(ids.size()); + state->slice = SavedMusicSlice( + std::move(ids), + count, + (hasBefore - takeBefore), + count - hasBefore - added); + consumer.put_next_copy(state->slice); + }; + const auto schedule = [=] { + if (state->scheduled) { + return; + } + state->scheduled = true; + Ui::PostponeCall(&state->guard, [=] { + if (state->scheduled) { + push(); + } + }); + }; + + const auto peerId = peer->id; + const auto savedMusic = &peer->owner().savedMusic(); + savedMusic->changed( + ) | rpl::filter( + rpl::mappers::_1 == peerId + ) | rpl::start_with_next(schedule, lifetime); + + if (!savedMusic->countKnown(peerId)) { + savedMusic->loadMore(peerId); + } + + push(); + + return lifetime; + }; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_music.h b/Telegram/SourceFiles/data/data_saved_music.h new file mode 100644 index 0000000000..8d2ab338bd --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_music.h @@ -0,0 +1,65 @@ +/* +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 "data/data_abstract_sparse_ids.h" + +class PeerData; + +namespace Data { + +class Session; + +class SavedMusic final { +public: + explicit SavedMusic(not_null owner); + + [[nodiscard]] static bool Supported(PeerId peerId); + + [[nodiscard]] bool countKnown(PeerId peerId) const; + [[nodiscard]] int count(PeerId peerId) const; + [[nodiscard]] const std::vector> &list( + PeerId peerId) const; + void loadMore(PeerId peerId); + + [[nodiscard]] rpl::producer changed() const; + + [[nodiscard]] bool has(not_null document) const; + void save(not_null document); + void remove(not_null document); + +private: + struct Entry { + std::vector> list; + mtpRequestId requestId = 0; + int total = -1; + bool loaded = false; + bool reloading = false; + }; + + void loadMore(PeerId peerId, bool reload); + [[nodiscard]] Entry *lookupEntry(PeerId peerId); + [[nodiscard]] const Entry *lookupEntry(PeerId peerId) const; + [[nodiscard]] uint64 firstPageHash(const Entry &entry) const; + + const not_null _owner; + + std::unordered_map _entries; + rpl::event_stream _changed; + +}; + +using SavedMusicSlice = AbstractSparseIds< + std::vector>>; + +[[nodiscard]] rpl::producer SavedMusicList( + not_null peer, + DocumentData *aroundId, + int limit); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 228840c06b..a7009dfb68 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -66,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_icons.h" #include "data/data_cloud_themes.h" #include "data/data_saved_messages.h" +#include "data/data_saved_music.h" #include "data/data_saved_sublist.h" #include "data/data_stories.h" #include "data/data_streaming.h" @@ -250,6 +251,7 @@ Session::Session(not_null session) , _notifySettings(std::make_unique(this)) , _customEmojiManager(std::make_unique(this)) , _stories(std::make_unique(this)) +, _savedMusic(std::make_unique(this)) , _savedMessages(std::make_unique(this)) , _chatbots(std::make_unique(this)) , _businessInfo(std::make_unique(this)) diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 6daa540e5f..fba78f17ba 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -66,6 +66,7 @@ class GroupCall; class NotifySettings; class CustomEmojiManager; class Stories; +class SavedMusic; class SavedMessages; class Chatbots; class BusinessInfo; @@ -184,6 +185,9 @@ public: [[nodiscard]] Stories &stories() const { return *_stories; } + [[nodiscard]] SavedMusic &savedMusic() const { + return *_savedMusic; + } [[nodiscard]] SavedMessages &savedMessages() const { return *_savedMessages; } @@ -1235,6 +1239,7 @@ private: const std::unique_ptr _notifySettings; const std::unique_ptr _customEmojiManager; const std::unique_ptr _stories; + const std::unique_ptr _savedMusic; const std::unique_ptr _savedMessages; const std::unique_ptr _chatbots; const std::unique_ptr _businessInfo; diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index c792aa0826..5d7c335194 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -1721,7 +1721,7 @@ rpl::producer Stories::albumIdsChanged() const { int Stories::albumIdsCount(PeerId peerId, int albumId) const { const auto set = albumIdsSet(peerId, albumId); - return set ? set->total : 0; + return set ? std::max(set->total, 0) : 0; } bool Stories::albumIdsCountKnown(PeerId peerId, int albumId) const { diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index ee963abd0c..be9932b6d5 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -86,6 +86,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "data/components/factchecks.h" #include "data/components/sponsored_messages.h" +#include "data/data_saved_music.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_document.h" @@ -2550,6 +2551,20 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { saveDocumentToFile(itemId, document); }), &st::menuIconDownload); + if (document->isSong()) { + if (document->owner().savedMusic().has(document)) { + _menu->addAction(u"remove muzlo :("_q, [=] { + document->owner().savedMusic().remove(document); + _controller->showToast(u"removed from muzlo!"_q); + }, &st::menuIconUnfave); + } else { + _menu->addAction(u"to muzlo!"_q, [=] { + document->owner().savedMusic().save(document); + _controller->showToast(u"added to muzlo! :)"_q); + }, &st::menuIconFave); + } + } + HistoryView::AddCopyFilename( _menu, document, diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index c696a6973e..075bd6a7e2 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_widget.h" #include "info/common_groups/info_common_groups_widget.h" #include "info/peer_gifts/info_peer_gifts_common.h" +#include "info/saved/info_saved_music_common.h" #include "info/stories/info_stories_common.h" #include "info/info_layer_widget.h" #include "info/info_section_widget.h" @@ -489,6 +490,8 @@ Key ContentMemento::key() const { storiesAlbumId(), storiesAddToAlbumId(), }; + } else if (const auto music = musicPeer()) { + return Saved::MusicTag{ music }; } else if (statisticsTag().peer) { return statisticsTag(); } else if (const auto starref = starrefPeer()) { @@ -536,6 +539,10 @@ ContentMemento::ContentMemento(Stories::Tag stories) , _storiesAddToAlbumId(stories.addingToAlbumId) { } +ContentMemento::ContentMemento(Saved::MusicTag music) +: _musicPeer(music.peer) { +} + ContentMemento::ContentMemento(PeerGifts::Tag gifts) : _giftsPeer(gifts.peer) , _giftsCollectionId(gifts.collectionId) { diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 647124d0b7..148d20ae39 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -68,6 +68,10 @@ namespace Info::Stories { struct Tag; } // namespace Info::Stories +namespace Info::Saved { +struct MusicTag; +} // namespace Info::Saved + namespace Info { class ContentMemento; @@ -219,6 +223,7 @@ public: explicit ContentMemento(Settings::Tag settings); explicit ContentMemento(Downloads::Tag downloads); explicit ContentMemento(Stories::Tag stories); + explicit ContentMemento(Saved::MusicTag music); explicit ContentMemento(Statistics::Tag statistics); explicit ContentMemento(BotStarRef::Tag starref); explicit ContentMemento(GlobalMedia::Tag global); @@ -261,6 +266,9 @@ public: [[nodiscard]] int storiesAddToAlbumId() const { return _storiesAddToAlbumId; } + [[nodiscard]] PeerData *musicPeer() const { + return _musicPeer; + } [[nodiscard]] PeerData *giftsPeer() const { return _giftsPeer; } @@ -333,6 +341,7 @@ private: PeerData * const _storiesPeer = nullptr; int _storiesAlbumId = 0; int _storiesAddToAlbumId = 0; + PeerData * const _musicPeer = nullptr; PeerData * const _giftsPeer = nullptr; int _giftsCollectionId = 0; Statistics::Tag _statisticsTag; diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index 64b171919c..42f8972a0a 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -49,6 +49,9 @@ Key::Key(Downloads::Tag downloads) : _value(downloads) { Key::Key(Stories::Tag stories) : _value(stories) { } +Key::Key(Saved::MusicTag music) : _value(music) { +} + Key::Key(Statistics::Tag statistics) : _value(statistics) { } @@ -135,6 +138,13 @@ int Key::storiesAddToAlbumId() const { return 0; } +PeerData *Key::musicPeer() const { + if (const auto tag = std::get_if(&_value)) { + return tag->peer; + } + return nullptr; +} + PeerData *Key::giftsPeer() const { if (const auto tag = std::get_if(&_value)) { return tag->peer; @@ -392,6 +402,7 @@ bool Controller::validateMementoPeer( && memento->migratedPeerId() == migratedPeerId() && memento->settingsSelf() == settingsSelf() && memento->storiesPeer() == storiesPeer() + && memento->musicPeer() == musicPeer() && memento->statisticsTag().peer == statisticsTag().peer && memento->starrefPeer() == starrefPeer() && memento->starrefType() == starrefType(); diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index 5c3bfe46a0..14ad24d686 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_message_reaction_id.h" #include "data/data_search_controller.h" #include "info/peer_gifts/info_peer_gifts_common.h" +#include "info/saved/info_saved_music_common.h" #include "info/statistics/info_statistics_tag.h" #include "info/stories/info_stories_common.h" #include "window/window_session_controller.h" @@ -82,6 +83,7 @@ public: Key(Settings::Tag settings); Key(Downloads::Tag downloads); Key(Stories::Tag stories); + Key(Saved::MusicTag music); Key(Statistics::Tag statistics); Key(PeerGifts::Tag gifts); Key(BotStarRef::Tag starref); @@ -101,6 +103,7 @@ public: [[nodiscard]] PeerData *storiesPeer() const; [[nodiscard]] int storiesAlbumId() const; [[nodiscard]] int storiesAddToAlbumId() const; + [[nodiscard]] PeerData *musicPeer() const; [[nodiscard]] PeerData *giftsPeer() const; [[nodiscard]] int giftsCollectionId() const; [[nodiscard]] Statistics::Tag statisticsTag() const; @@ -130,6 +133,7 @@ private: Settings::Tag, Downloads::Tag, Stories::Tag, + Saved::MusicTag, Statistics::Tag, PeerGifts::Tag, BotStarRef::Tag, @@ -160,6 +164,7 @@ public: Settings, Downloads, Stories, + SavedMusic, PollResults, Statistics, BotStarRef, @@ -239,6 +244,9 @@ public: [[nodiscard]] int storiesAddToAlbumId() const { return key().storiesAddToAlbumId(); } + [[nodiscard]] PeerData *musicPeer() const { + return key().musicPeer(); + } [[nodiscard]] PeerData *giftsPeer() const { return key().giftsPeer(); } diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.cpp b/Telegram/SourceFiles/info/media/info_media_buttons.cpp index 473a9558bd..f83ece49cc 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.cpp +++ b/Telegram/SourceFiles/info/media/info_media_buttons.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/ui_integration.h" #include "data/components/recent_shared_media_gifts.h" #include "data/data_channel.h" +#include "data/data_saved_music.h" #include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" @@ -25,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_memento.h" #include "info/peer_gifts/info_peer_gifts_widget.h" #include "info/profile/info_profile_values.h" +#include "info/saved/info_saved_music_widget.h" #include "info/stories/info_stories_widget.h" #include "main/main_session.h" #include "ui/text/text_utilities.h" @@ -392,4 +394,43 @@ not_null AddPeerGiftsButton( return wrap->entity(); } +not_null AddMusicButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer) { + auto count = rpl::single(0) | rpl::then(Data::SavedMusicList( + peer, + nullptr, + 0 + ) | rpl::map([](const Data::SavedMusicSlice &slice) { + return slice.fullCount().value_or(0); + })); + + using namespace ::Settings; + auto forked = std::move(count) + | start_spawning(parent->lifetime()); + auto text = rpl::duplicate( + forked + ) | rpl::map([](int count) { + AssertIsDebug(); + return QString::number(count) + u" muzla"_q; + }); + auto button = parent->add(object_ptr>( + parent, + object_ptr( + parent, + std::move(text), + st::infoSharedMediaButton)) + )->setDuration( + st::infoSlideDuration + )->toggleOn( + rpl::duplicate(forked) | rpl::map(rpl::mappers::_1 > 0) + )->entity(); + + button->addClickHandler([=] { + navigation->showSection(Info::Saved::MakeMusic(peer)); + }); + return button; +} + } // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h index a8a24feb47..161e59a573 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.h +++ b/Telegram/SourceFiles/info/media/info_media_buttons.h @@ -77,4 +77,9 @@ using Type = Storage::SharedMediaType; not_null peer, Ui::MultiSlideTracker &tracker); +[[nodiscard]] not_null AddMusicButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null peer); + } // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 3c62d16368..95c93ba3e4 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_provider.h" #include "info/media/info_media_list_section.h" #include "info/downloads/info_downloads_provider.h" +#include "info/saved/info_saved_music_provider.h" #include "info/stories/info_stories_provider.h" #include "info/info_controller.h" #include "layout/layout_mosaic.h" @@ -98,6 +99,8 @@ struct ListWidget::DateBadge { not_null controller) { if (controller->isDownloads()) { return std::make_unique(controller); + } else if (controller->musicPeer()) { + return std::make_unique(controller); } else if (controller->storiesPeer()) { return std::make_unique(controller); } else if (controller->section().type() == Section::Type::GlobalMedia) { @@ -196,6 +199,9 @@ void ListWidget::start() { setupStoriesTrackIds(); trackSession(&session()); restart(); + } else if (_controller->musicPeer()) { + trackSession(&session()); + restart(); } else { trackSession(&session()); diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index 5fe27a2eb7..a8faadb1e3 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_file_origin.h" #include "data/data_user.h" +#include "data/data_saved_music.h" #include "data/data_saved_sublist.h" #include "main/main_session.h" #include "apiwrap.h" @@ -80,6 +81,14 @@ object_ptr InnerWidget::setupContent( _cover = AddCover(result, _controller, _peer, _topic, _sublist); if (_topic && _topic->creating()) { return result; + } else if (Data::SavedMusic::Supported(_peer->id)) { + object_ptr( + Media::AddMusicButton( + result, + _controller, + _peer), + st::infoIconMediaAudio, + st::infoSharedMediaButtonIconPosition); } AddDetails(result, _controller, _peer, _topic, _sublist, origin); diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_common.h b/Telegram/SourceFiles/info/saved/info_saved_music_common.h new file mode 100644 index 0000000000..f40ffae8bc --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_music_common.h @@ -0,0 +1,20 @@ +/* +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 + +namespace Info::Saved { + +struct MusicTag { + explicit MusicTag(not_null peer) + : peer(peer) { + } + + not_null peer; +}; + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp b/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp new file mode 100644 index 0000000000..99fe45ba40 --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp @@ -0,0 +1,462 @@ +/* +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/saved/info_saved_music_provider.h" + +#include "base/unixtime.h" +#include "core/application.h" +#include "data/data_changes.h" +#include "data/data_channel.h" +#include "data/data_document.h" +#include "data/data_media_types.h" +#include "data/data_saved_music.h" +#include "data/data_session.h" +#include "history/history_item.h" +#include "history/history_item_helpers.h" +#include "history/history.h" +#include "info/media/info_media_widget.h" +#include "info/media/info_media_list_section.h" +#include "info/info_controller.h" +#include "main/main_account.h" +#include "main/main_session.h" +#include "layout/layout_selection.h" +#include "storage/storage_shared_media.h" +#include "styles/style_info.h" +#include "styles/style_overview.h" + +namespace Info::Saved { +namespace { + +using namespace Media; + +constexpr auto kPreloadedScreensCount = 4; +constexpr auto kPreloadedScreensCountFull + = kPreloadedScreensCount + 1 + kPreloadedScreensCount; + +[[nodiscard]] int MinStoryHeight(int width) { + auto itemsLeft = st::infoMediaSkip; + auto itemsInRow = (width - itemsLeft) + / (st::infoMediaMinGridSize + st::infoMediaSkip); + return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow; +} + +} // namespace + +MusicProvider::MusicProvider(not_null controller) +: _controller(controller) +, _peer(controller->key().musicPeer()) +, _history(_peer->owner().history(_peer)) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + for (auto &layout : _layouts) { + layout.second.item->invalidateCache(); + } + }, _lifetime); +} + +MusicProvider::~MusicProvider() { + clear(); +} + +Type MusicProvider::type() { + return Type::MusicFile; +} + +bool MusicProvider::hasSelectRestriction() { + if (_peer->session().frozen()) { + return true; + } + return !_peer->isSelf(); +} + +rpl::producer MusicProvider::hasSelectRestrictionChanges() { + return rpl::never(); +} + +bool MusicProvider::sectionHasFloatingHeader() { + return false; +} + +not_null MusicProvider::musicIdFromMsgId(MsgId itemId) const { + const auto i = _musicIdFromMsgId.find(itemId); + Assert(i != end(_musicIdFromMsgId)); + + return i->second; +} + +not_null MusicProvider::musicIdToMsg( + not_null id) const { + const auto i = _musicIdToMsg.find(id); + if (i != end(_musicIdToMsg)) { + return i->second.get(); + } + return _musicIdToMsg.emplace(id, _history->makeMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem | MessageFlag::HasFromId), + .from = _peer->id, + .date = base::unixtime::now(), + }, id, TextWithEntities())).first->second.get(); +} + +MsgId MusicProvider::musicIdToMsgId(not_null id) const { + return musicIdToMsg(id)->id; +} + +QString MusicProvider::sectionTitle(not_null item) { + return QString(); +} + +bool MusicProvider::sectionItemBelongsHere( + not_null item, + not_null previous) { + return true; +} + +bool MusicProvider::isPossiblyMyItem(not_null item) { + return true; +} + +std::optional MusicProvider::fullCount() { + return _slice.fullCount(); +} + +void MusicProvider::clear() { + _layouts.clear(); + _aroundId = nullptr; + _idsLimit = kMinimalIdsLimit; + _slice = Data::SavedMusicSlice(); +} + +void MusicProvider::restart() { + clear(); + refreshViewer(); +} + +void MusicProvider::checkPreload( + QSize viewport, + not_null topLayout, + not_null bottomLayout, + bool preloadTop, + bool preloadBottom) { + const auto visibleWidth = viewport.width(); + const auto visibleHeight = viewport.height(); + const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight; + const auto minItemHeight = MinStoryHeight(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 minIdDelta = (minScreenDelta * visibleHeight) + / minItemHeight; + const auto preloadAroundItem = [&](not_null layout) { + auto preloadRequired = false; + const auto id = musicIdFromMsgId(layout->getItem()->id); + if (!preloadRequired) { + preloadRequired = (_idsLimit < preloadIdsLimitMin); + } + if (!preloadRequired) { + auto delta = _slice.distance(_aroundId, id); + Assert(delta != std::nullopt); + preloadRequired = (qAbs(*delta) >= minIdDelta); + } + if (preloadRequired) { + _idsLimit = preloadIdsLimit; + _aroundId = id; + refreshViewer(); + } + }; + + if (preloadTop && !topLoaded) { + preloadAroundItem(topLayout); + } else if (preloadBottom && !bottomLoaded) { + preloadAroundItem(bottomLayout); + } +} + +void MusicProvider::setSearchQuery(QString query) { +} + +void MusicProvider::refreshViewer() { + _viewerLifetime.destroy(); + const auto aroundId = _aroundId; + auto ids = Data::SavedMusicList(_peer, aroundId, _idsLimit); + std::move( + ids + ) | rpl::start_with_next([=](Data::SavedMusicSlice &&slice) { + if (!slice.fullCount()) { + // Don't display anything while full count is unknown. + return; + } + _slice = std::move(slice); + + auto nearestId = (DocumentData*)nullptr; + for (auto i = 0; i != _slice.size(); ++i) { + if (_slice[i] == aroundId) { + nearestId = aroundId; + break; + } + } + if (!nearestId && _slice.size() > 0) { + _aroundId = _slice[_slice.size() / 2]; + } + _refreshed.fire({}); + }, _viewerLifetime); +} + +rpl::producer<> MusicProvider::refreshed() { + return _refreshed.events(); +} + +std::vector MusicProvider::fillSections( + not_null delegate) { + markLayoutsStale(); + const auto guard = gsl::finally([&] { clearStaleLayouts(); }); + + auto result = std::vector(); + auto section = ListSection(Type::MusicFile, sectionDelegate()); + auto count = _slice.size(); + for (auto i = 0; i != count; ++i) { + const auto musicId = _slice[i]; + if (const auto layout = getLayout(musicId, delegate)) { + if (!section.addItem(layout)) { + section.finishSection(); + result.push_back(std::move(section)); + section = ListSection(Type::MusicFile, sectionDelegate()); + section.addItem(layout); + } + } + } + if (!section.empty()) { + section.finishSection(); + result.push_back(std::move(section)); + } + return result; +} + +void MusicProvider::markLayoutsStale() { + for (auto &layout : _layouts) { + layout.second.stale = true; + } +} + +void MusicProvider::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> MusicProvider::layoutRemoved() { + return _layoutRemoved.events(); +} + +BaseLayout *MusicProvider::lookupLayout(const HistoryItem *item) { + return nullptr; +} + +bool MusicProvider::isMyItem(not_null item) { + return IsStoryMsgId(item->id) && (item->history()->peer == _peer); +} + +bool MusicProvider::isAfter( + not_null a, + not_null b) { + return (a->id < b->id); +} + +BaseLayout *MusicProvider::getLayout( + not_null id, + not_null delegate) { + auto it = _layouts.find(id); + if (it == _layouts.end()) { + if (auto layout = createLayout(id, delegate)) { + layout->initDimensions(); + it = _layouts.emplace(id, std::move(layout)).first; + } else { + return nullptr; + } + } + it->second.stale = false; + return it->second.item.get(); +} + +std::unique_ptr MusicProvider::createLayout( + not_null id, + not_null delegate) { + const auto item = musicIdToMsg(id); + if (!item) { + return nullptr; + } + const auto getPhoto = [&]() -> PhotoData* { + if (const auto media = item->media()) { + return media->photo(); + } + return nullptr; + }; + const auto getFile = [&]() -> DocumentData* { + if (const auto media = item->media()) { + return media->document(); + } + return nullptr; + }; + + const auto peer = item->history()->peer; + + using namespace Overview::Layout; + const auto options = MediaOptions{ + }; + if (const auto file = getFile()) { + return std::make_unique( + delegate, + item, + DocumentFields{ file }, + st::overviewFileLayout); + } + return nullptr; +} + +ListItemSelectionData MusicProvider::computeSelectionData( + not_null item, + TextSelection selection) { + auto result = ListItemSelectionData(selection); + const auto id = item->id; + if (!_musicIdFromMsgId.contains(id)) { + return result; + } + AssertIsDebug(); + //const auto peer = item->history()->peer; + //const auto channel = peer->asChannel(); + //const auto maybeStory = peer->owner().stories().lookup( + // { peer->id, StoryIdFromMsgId(id) }); + //if (maybeStory) { + // const auto story = *maybeStory; + // result.canForward = peer->isSelf() && story->canShare(); + // result.canDelete = story->canDelete(); + // result.canUnpinStory = story->pinnedToTop(); + // result.storyInProfile = story->inProfile(); + //} + //result.canToggleStoryPin = peer->isSelf() + // || (channel && channel->canEditStories()); + return result; +} + +void MusicProvider::applyDragSelection( + ListSelectedMap &selected, + not_null fromItem, + bool skipFrom, + not_null tillItem, + bool skipTill) { + const auto fromId = fromItem->id - (skipFrom ? 1 : 0); + const auto tillId = tillItem->id - (skipTill ? 0 : 1); + for (auto i = selected.begin(); i != selected.end();) { + const auto itemId = i->first->id; + if (itemId > fromId || itemId <= tillId) { + i = selected.erase(i); + } else { + ++i; + } + } + for (auto &layoutItem : _layouts) { + const auto musicId = layoutItem.first; + const auto item = musicIdToMsg(musicId); + if (item->id <= fromId && item->id > tillId) { + ChangeItemSelection( + selected, + item, + computeSelectionData(item, FullSelection)); + } + } +} + +bool MusicProvider::allowSaveFileAs( + not_null item, + not_null document) { + return false; +} + +QString MusicProvider::showInFolderPath( + not_null item, + not_null document) { + return QString(); +} + +int64 MusicProvider::scrollTopStatePosition(not_null item) { + return StoryIdFromMsgId(item->id); +} + +HistoryItem *MusicProvider::scrollTopStateItem(ListScrollTopState state) { + if (state.item && _slice.indexOf(musicIdFromMsgId(state.item->id))) { + return state.item; + //} else if (const auto id = _slice.nearest(state.position)) { + // const auto full = FullMsgId(_peer->id, StoryIdToMsgId(*id)); + // if (const auto item = _controller->session().data().message(full)) { + // return item; + // } + } + + auto nearestId = (DocumentData*)nullptr; AssertIsDebug(); + //for (auto i = 0; i != _slice.size(); ++i) { + // if (!nearestId + // || std::abs(*nearestId - state.position) + // > std::abs(_slice[i] - state.position)) { + // nearestId = _slice[i]; + // } + //} + if (nearestId) { + const auto full = FullMsgId(_peer->id, musicIdToMsgId(nearestId)); + if (const auto item = _controller->session().data().message(full)) { + return item; + } + } + + return state.item; +} + +void MusicProvider::saveState( + not_null memento, + ListScrollTopState scrollState) { + if (_aroundId != nullptr && scrollState.item) { + AssertIsDebug(); + //memento->setAroundId({ _peer->id, StoryIdToMsgId(_aroundId) }); + //memento->setIdsLimit(_idsLimit); + //memento->setScrollTopItem(scrollState.item->globalId()); + //memento->setScrollTopItemPosition(scrollState.position); + //memento->setScrollTopShift(scrollState.shift); + } +} + +void MusicProvider::restoreState( + not_null memento, + Fn restoreScrollState) { + if (const auto limit = memento->idsLimit()) { + const auto wasAroundId = memento->aroundId(); + if (wasAroundId.peer == _peer->id) { + _idsLimit = limit; + AssertIsDebug(); + //_aroundId = StoryIdFromMsgId(wasAroundId.msg); + restoreScrollState({ + .position = memento->scrollTopItemPosition(), + .item = MessageByGlobalId(memento->scrollTopItem()), + .shift = memento->scrollTopShift(), + }); + refreshViewer(); + } + } +} + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_provider.h b/Telegram/SourceFiles/info/saved/info_saved_music_provider.h new file mode 100644 index 0000000000..483d6e6a0e --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_music_provider.h @@ -0,0 +1,141 @@ +/* +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 "base/weak_ptr.h" +#include "data/data_saved_music.h" +#include "info/media/info_media_common.h" +#include "info/saved/info_saved_music_common.h" +#include "history/history_item.h" + +class DocumentData; +class HistoryItem; +class PeerData; +class History; + +namespace Info { +class AbstractController; +} // namespace Info + +namespace Info::Saved { + +class MusicProvider final + : public Media::ListProvider + , private Media::ListSectionDelegate + , public base::has_weak_ptr { +public: + explicit MusicProvider(not_null controller); + ~MusicProvider(); + + Media::Type type() override; + bool hasSelectRestriction() override; + rpl::producer hasSelectRestrictionChanges() override; + bool isPossiblyMyItem(not_null item) override; + + std::optional fullCount() override; + + void restart() override; + void checkPreload( + QSize viewport, + not_null topLayout, + not_null bottomLayout, + bool preloadTop, + bool preloadBottom) override; + void refreshViewer() override; + rpl::producer<> refreshed() override; + + void setSearchQuery(QString query) override; + + std::vector fillSections( + not_null delegate) override; + rpl::producer> layoutRemoved() override; + Media::BaseLayout *lookupLayout(const HistoryItem *item) override; + bool isMyItem(not_null item) override; + bool isAfter( + not_null a, + not_null b) override; + + Media::ListItemSelectionData computeSelectionData( + not_null item, + TextSelection selection) override; + void applyDragSelection( + Media::ListSelectedMap &selected, + not_null fromItem, + bool skipFrom, + not_null tillItem, + bool skipTill) override; + + bool allowSaveFileAs( + not_null item, + not_null document) override; + QString showInFolderPath( + not_null item, + not_null document) override; + + int64 scrollTopStatePosition(not_null item) override; + HistoryItem *scrollTopStateItem( + Media::ListScrollTopState state) override; + void saveState( + not_null memento, + Media::ListScrollTopState scrollState) override; + void restoreState( + not_null memento, + Fn restoreScrollState) override; + +private: + static constexpr auto kMinimalIdsLimit = 16; + + using OwnedItem = std::unique_ptr; + + [[nodiscard]] not_null musicIdFromMsgId( + MsgId itemId) const; + [[nodiscard]] not_null musicIdToMsg( + not_null id) const; + [[nodiscard]] MsgId musicIdToMsgId(not_null id) const; + + bool sectionHasFloatingHeader() override; + QString sectionTitle(not_null item) override; + bool sectionItemBelongsHere( + not_null item, + not_null previous) override; + + void markLayoutsStale(); + void clearStaleLayouts(); + void clear(); + + [[nodiscard]] HistoryItem *ensureItem(not_null id); + [[nodiscard]] Media::BaseLayout *getLayout( + not_null id, + not_null delegate); + [[nodiscard]] std::unique_ptr createLayout( + not_null musicId, + not_null delegate); + + const not_null _controller; + const not_null _peer; + const not_null _history; + + DocumentData *_aroundId = nullptr; + int _idsLimit = kMinimalIdsLimit; + Data::SavedMusicSlice _slice; + + std::unordered_map, Media::CachedItem> _layouts; + rpl::event_stream> _layoutRemoved; + rpl::event_stream<> _refreshed; + + mutable base::flat_map> _musicIdFromMsgId; + mutable base::flat_map, OwnedItem> _musicIdToMsg; + + bool _started = false; + + rpl::lifetime _lifetime; + rpl::lifetime _viewerLifetime; + +}; + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_widget.cpp b/Telegram/SourceFiles/info/saved/info_saved_music_widget.cpp new file mode 100644 index 0000000000..762e252aed --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_music_widget.cpp @@ -0,0 +1,339 @@ +/* +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/saved/info_saved_music_widget.h" + +#include "data/data_document.h" +#include "data/data_peer.h" +#include "data/data_saved_music.h" +#include "data/data_session.h" +#include "info/media/info_media_list_widget.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "main/main_session.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/scroll_area.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "lang/lang_keys.h" +#include "ui/ui_utility.h" +#include "styles/style_credits.h" // giftListAbout +#include "styles/style_info.h" +#include "styles/style_layers.h" + +namespace Info::Saved { + +class MusicInner final : public Ui::RpWidget { +public: + MusicInner(QWidget *parent, not_null controller); + ~MusicInner(); + + bool showInternal(not_null memento); + void setIsStackBottom(bool isStackBottom) { + _isStackBottom = isStackBottom; + } + + void saveState(not_null memento); + void restoreState(not_null memento); + + void setScrollHeightValue(rpl::producer value); + + rpl::producer scrollToRequests() const; + rpl::producer selectedListValue() const; + void selectionAction(SelectionAction action); + +protected: + int resizeGetHeight(int newWidth) override; + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override; + +private: + int recountHeight(); + void refreshHeight(); + void refreshEmpty(); + + void setupList(); + void setupEmpty(); + + const not_null _controller; + const not_null _peer; + + base::unique_qptr _menu; + + object_ptr _list = { nullptr }; + object_ptr _empty = { nullptr }; + int _lastNonLoadingHeight = 0; + bool _emptyLoading = false; + + bool _inResize = false; + bool _isStackBottom = false; + + rpl::event_stream _scrollToRequests; + rpl::event_stream> _selectedLists; + rpl::event_stream> _listTops; + rpl::variable _topHeight; + rpl::variable _albumEmpty; + +}; + +MusicInner::MusicInner(QWidget *parent, not_null controller) +: RpWidget(parent) +, _controller(controller) +, _peer(controller->key().musicPeer()) { + setupList(); + setupEmpty(); +} + +MusicInner::~MusicInner() = default; + +void MusicInner::visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) { + setChildVisibleTopBottom(_list, visibleTop, visibleBottom); +} + +bool MusicInner::showInternal(not_null memento) { + if (memento->section().type() == Section::Type::SavedMusic) { + restoreState(memento); + return true; + } + return false; +} + +void MusicInner::setupList() { + Expects(!_list); + + _list = object_ptr(this, _controller); + const auto raw = _list.data(); + + using namespace rpl::mappers; + raw->scrollToRequests( + ) | rpl::map([=](int to) { + return Ui::ScrollToRequest { + raw->y() + to, + -1 + }; + }) | rpl::start_to_stream(_scrollToRequests, raw->lifetime()); + _selectedLists.fire(raw->selectedListValue()); + _listTops.fire(raw->topValue()); + + raw->show(); +} + +void MusicInner::setupEmpty() { + _list->resizeToWidth(width()); + + const auto savedMusic = &_controller->session().data().savedMusic(); + rpl::combine( + rpl::single( + rpl::empty + ) | rpl::then( + savedMusic->changed() | rpl::filter( + rpl::mappers::_1 == _peer->id + ) | rpl::to_empty + ), + _list->heightValue() + ) | rpl::start_with_next([=](auto, int listHeight) { + const auto padding = st::infoMediaMargin; + if (const auto raw = _empty.release()) { + raw->hide(); + raw->deleteLater(); + } + _emptyLoading = false; + if (listHeight <= padding.bottom() + padding.top()) { + refreshEmpty(); + } else { + _albumEmpty = false; + } + refreshHeight(); + }, _list->lifetime()); +} + +void MusicInner::refreshEmpty() { + const auto savedMusic = &_controller->session().data().savedMusic(); + const auto knownEmpty = savedMusic->countKnown(_peer->id); + _empty = object_ptr( + this, + (!knownEmpty AssertIsDebug() + ? tr::lng_contacts_loading(Ui::Text::WithEntities) + : rpl::single(TextWithEntities{ u"no muzlo found :("_q })), + st::giftListAbout); + _empty->show(); + _emptyLoading = !knownEmpty; + resizeToWidth(width()); +} + +void MusicInner::saveState(not_null memento) { + _list->saveState(&memento->media()); +} + +void MusicInner::restoreState(not_null memento) { + _list->restoreState(&memento->media()); +} + +rpl::producer MusicInner::selectedListValue() const { + return _selectedLists.events_starting_with( + _list->selectedListValue() + ) | rpl::flatten_latest(); +} + +void MusicInner::selectionAction(SelectionAction action) { + _list->selectionAction(action); +} + +int MusicInner::resizeGetHeight(int newWidth) { + if (!newWidth) { + return 0; + } + _inResize = true; + auto guard = gsl::finally([this] { _inResize = false; }); + + if (_list) { + _list->resizeToWidth(newWidth); + } + if (const auto empty = _empty.get()) { + const auto margin = st::giftListAboutMargin; + empty->resizeToWidth(newWidth - margin.left() - margin.right()); + } + + return recountHeight(); +} + +void MusicInner::refreshHeight() { + if (_inResize) { + return; + } + resize(width(), recountHeight()); +} + +int MusicInner::recountHeight() { + auto top = 0; + auto listHeight = 0; + if (_list) { + _list->moveToLeft(0, top); + listHeight = _list->heightNoMargins(); + top += listHeight; + } + if (const auto empty = _empty.get()) { + const auto margin = st::giftListAboutMargin; + empty->moveToLeft(margin.left(), top + margin.top()); + top += margin.top() + empty->height() + margin.bottom(); + } + if (_emptyLoading) { + top = std::max(top, _lastNonLoadingHeight); + } else { + _lastNonLoadingHeight = top; + } + return top; +} + +void MusicInner::setScrollHeightValue(rpl::producer value) { +} + +rpl::producer MusicInner::scrollToRequests() const { + return _scrollToRequests.events(); +} + +MusicMemento::MusicMemento(not_null controller) +: ContentMemento(MusicTag{ controller->musicPeer() }) +, _media(controller) { +} + +MusicMemento::MusicMemento(not_null peer) +: ContentMemento(MusicTag{ peer }) +, _media(peer, 0, Media::Type::MusicFile) { +} + +MusicMemento::~MusicMemento() = default; + +Section MusicMemento::section() const { + return Section(Section::Type::SavedMusic); +} + +object_ptr MusicMemento::createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) { + auto result = object_ptr(parent, controller); + result->setInternalState(geometry, this); + return result; +} + +MusicWidget::MusicWidget( + QWidget *parent, + not_null controller) +: ContentWidget(parent, controller) { + _inner = setInnerWidget(object_ptr(this, controller)); + _inner->setScrollHeightValue(scrollHeightValue()); + _inner->scrollToRequests( + ) | rpl::start_with_next([this](Ui::ScrollToRequest request) { + scrollTo(request); + }, _inner->lifetime()); +} + +void MusicWidget::setIsStackBottom(bool isStackBottom) { + ContentWidget::setIsStackBottom(isStackBottom); + _inner->setIsStackBottom(isStackBottom); +} + +bool MusicWidget::showInternal(not_null memento) { + if (!controller()->validateMementoPeer(memento)) { + return false; + } + return true; +} + +void MusicWidget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +std::shared_ptr MusicWidget::doCreateMemento() { + auto result = std::make_shared(controller()); + saveState(result.get()); + return result; +} + +void MusicWidget::saveState(not_null memento) { + memento->setScrollTop(scrollTopSave()); + _inner->saveState(memento); +} + +void MusicWidget::restoreState(not_null memento) { + _inner->restoreState(memento); + scrollTopRestore(memento->scrollTop()); +} + +rpl::producer MusicWidget::selectedListValue() const { + return _inner->selectedListValue(); +} + +void MusicWidget::selectionAction(SelectionAction action) { + _inner->selectionAction(action); +} + +rpl::producer MusicWidget::title() { + //const auto peer = controller()->key().musicPeer(); + AssertIsDebug(); + return rpl::single(u"muzlo"_q); +} + +std::shared_ptr MakeMusic(not_null peer) { + return std::make_shared( + std::vector>( + 1, + std::make_shared(peer))); +} + +} // namespace Info::Saved + diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_widget.h b/Telegram/SourceFiles/info/saved/info_saved_music_widget.h new file mode 100644 index 0000000000..5ecef46511 --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_music_widget.h @@ -0,0 +1,81 @@ +/* +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" +#include "info/stories/info_stories_common.h" + +namespace Ui { +template +class SlideWrap; +} // namespace Ui + +namespace Info::Saved { + +class MusicInner; + +class MusicMemento final : public ContentMemento { +public: + MusicMemento(not_null controller); + MusicMemento(not_null peer); + ~MusicMemento(); + + object_ptr createWidget( + QWidget *parent, + not_null 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; + int _addingToAlbumId = 0; + +}; + +class MusicWidget final : public ContentWidget { +public: + MusicWidget(QWidget *parent, not_null controller); + + void setIsStackBottom(bool isStackBottom) override; + + bool showInternal( + not_null memento) override; + + void setInternalState( + const QRect &geometry, + not_null memento); + + rpl::producer selectedListValue() const override; + void selectionAction(SelectionAction action) override; + + rpl::producer title() override; + +private: + void saveState(not_null memento); + void restoreState(not_null memento); + + std::shared_ptr doCreateMemento() override; + + MusicInner *_inner = nullptr; + bool _shown = false; + +}; + +[[nodiscard]] std::shared_ptr MakeMusic( + not_null peer); + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp index 75040c96cc..67e4515d9a 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "boxes/share_box.h" #include "data/data_peer.h" +#include "data/data_saved_music.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" @@ -291,6 +292,15 @@ void InnerWidget::createProfileTop() { using namespace Profile; AddCover(_top, _controller, _peer, nullptr, nullptr); + if (Data::SavedMusic::Supported(_peer->id)) { + object_ptr( + Media::AddMusicButton( + _top, + _controller, + _peer), + st::infoIconMediaAudio, + st::infoSharedMediaButtonIconPosition); + } AddDetails(_top, _controller, _peer, nullptr, nullptr, { v::null }); auto tracker = Ui::MultiSlideTracker(); diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp index 844ece18a0..e0e07fdadd 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp @@ -23,7 +23,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "history/history.h" #include "core/application.h" -#include "storage/storage_shared_media.h" #include "layout/layout_selection.h" #include "styles/style_info.h"