From 7717de19abefac256f95d26dcd4bc4d3e53ec038 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 7 May 2023 00:20:21 +0400 Subject: [PATCH] Implement stories switching, photo "animation". --- Telegram/SourceFiles/data/data_stories.cpp | 4 +- Telegram/SourceFiles/data/data_stories.h | 18 ++ .../stories/media_stories_controller.cpp | 179 ++++++++++++- .../media/stories/media_stories_controller.h | 42 ++- .../media/stories/media_stories_delegate.h | 12 + .../media/stories/media_stories_header.cpp | 8 + .../media/stories/media_stories_slider.cpp | 123 +++++++-- .../media/stories/media_stories_slider.h | 19 ++ .../media/stories/media_stories_view.cpp | 24 ++ .../media/stories/media_stories_view.h | 14 + .../SourceFiles/media/view/media_view.style | 15 +- .../media/view/media_view_overlay_widget.cpp | 248 ++++++++++++------ .../media/view/media_view_overlay_widget.h | 6 +- 13 files changed, 581 insertions(+), 131 deletions(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 0a0fe8b02..c5634e4ac 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -179,7 +179,6 @@ StoryId Stories::generate( 32, 32 )) | rpl::start_with_next([&](SharedMediaResult &&result) { - stories.total = result.count.value_or(1); if (!result.messageIds.contains(itemId)) { result.messageIds.emplace(itemId); } @@ -214,6 +213,9 @@ StoryId Stories::generate( } } } + stories.total = std::max( + result.count.value_or(1), + int(result.messageIds.size())); const auto i = ranges::find(_all, stories.user, &StoriesList::user); if (i != end(_all)) { *i = std::move(stories); diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index ea3a033f1..26f1f76a2 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -15,10 +15,13 @@ namespace Data { class Session; struct StoryPrivacy { + friend inline bool operator==(StoryPrivacy, StoryPrivacy) = default; }; struct StoryMedia { std::variant, not_null> data; + + friend inline bool operator==(StoryMedia, StoryMedia) = default; }; struct StoryItem { @@ -27,12 +30,27 @@ struct StoryItem { TextWithEntities caption; TimeId date = 0; StoryPrivacy privacy; + + friend inline bool operator==(StoryItem, StoryItem) = default; }; struct StoriesList { not_null user; std::vector items; int total = 0; + + friend inline bool operator==(StoriesList, StoriesList) = default; +}; + +struct FullStoryId { + UserData *user = nullptr; + StoryId id = 0; + + explicit operator bool() const { + return user != nullptr && id != 0; + } + friend inline auto operator<=>(FullStoryId, FullStoryId) = default; + friend inline bool operator==(FullStoryId, FullStoryId) = default; }; class Stories final { diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 3fbbdb1ef..f88ebf8d3 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -7,17 +7,95 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_controller.h" +#include "base/timer.h" +#include "base/power_save_blocker.h" #include "data/data_stories.h" #include "media/stories/media_stories_delegate.h" #include "media/stories/media_stories_header.h" #include "media/stories/media_stories_slider.h" #include "media/stories/media_stories_reply.h" +#include "media/audio/media_audio.h" #include "ui/rp_widget.h" #include "styles/style_media_view.h" #include "styles/style_widgets.h" #include "styles/style_boxes.h" // UserpicButton namespace Media::Stories { +namespace { + +constexpr auto kPhotoProgressInterval = crl::time(100); +constexpr auto kPhotoDuration = 5 * crl::time(1000); + +} // namespace + +class Controller::PhotoPlayback final { +public: + explicit PhotoPlayback(not_null controller); + + [[nodiscard]] bool paused() const; + void togglePaused(bool paused); + +private: + void callback(); + + const not_null _controller; + + base::Timer _timer; + crl::time _started = 0; + crl::time _paused = 0; + +}; + +Controller::PhotoPlayback::PhotoPlayback(not_null controller) +: _controller(controller) +, _timer([=] { callback(); }) +, _started(crl::now()) +, _paused(_started) { +} + +bool Controller::PhotoPlayback::paused() const { + return _paused != 0; +} + +void Controller::PhotoPlayback::togglePaused(bool paused) { + if (!_paused == !paused) { + return; + } else if (paused) { + const auto now = crl::now(); + if (now - _started >= kPhotoDuration) { + return; + } + _paused = now; + _timer.cancel(); + } else { + _started += crl::now() - _paused; + _paused = 0; + _timer.callEach(kPhotoProgressInterval); + } + callback(); +} + +void Controller::PhotoPlayback::callback() { + const auto now = crl::now(); + const auto elapsed = now - _started; + const auto finished = (now - _started >= kPhotoDuration); + if (finished) { + _timer.cancel(); + } + using State = Player::State; + const auto state = finished + ? State::StoppedAtEnd + : _paused + ? State::Paused + : State::Playing; + _controller->updatePhotoPlayback({ + .state = state, + .position = elapsed, + .receivedTill = kPhotoDuration, + .length = kPhotoDuration, + .frequency = 1000, + }); +} Controller::Controller(not_null delegate) : _delegate(delegate) @@ -28,12 +106,14 @@ Controller::Controller(not_null delegate) initLayout(); } +Controller::~Controller() = default; + void Controller::initLayout() { const auto headerHeight = st::storiesHeaderMargin.top() + st::storiesHeaderPhoto.photoSize + st::storiesHeaderMargin.bottom(); const auto sliderHeight = st::storiesSliderMargin.top() - + st::storiesSlider.width + + st::storiesSliderWidth + st::storiesSliderMargin.bottom(); const auto outsideHeaderHeight = headerHeight + sliderHeight; const auto fieldMinHeight = st::storiesFieldMargin.top() @@ -42,6 +122,7 @@ void Controller::initLayout() { const auto minHeightForOutsideHeader = st::storiesMaxSize.height() + outsideHeaderHeight + fieldMinHeight; + _layout = _wrap->sizeValue( ) | rpl::map([=](QSize size) { size = QSize( @@ -137,8 +218,19 @@ void Controller::show(const Data::StoriesList &list, int index) { Expects(index < list.items.size()); const auto &item = list.items[index]; + const auto guard = gsl::finally([&] { + if (v::is>(item.media.data)) { + _photoPlayback = std::make_unique(this); + } else { + _photoPlayback = nullptr; + } + }); + if (_list != list) { + _list = list; + } + _index = index; - const auto id = ShownId{ + const auto id = Data::FullStoryId{ .user = list.user, .id = item.id, }; @@ -152,4 +244,87 @@ void Controller::show(const Data::StoriesList &list, int index) { _replyArea->show({ .user = list.user }); } +void Controller::ready() { + if (_photoPlayback) { + _photoPlayback->togglePaused(false); + } +} + +void Controller::updateVideoPlayback(const Player::TrackState &state) { + updatePlayback(state); +} + +void Controller::updatePhotoPlayback(const Player::TrackState &state) { + updatePlayback(state); +} + +void Controller::updatePlayback(const Player::TrackState &state) { + _slider->updatePlayback(state); + updatePowerSaveBlocker(state); + if (Player::IsStoppedAtEnd(state.state)) { + if (!jumpFor(1)) { + _delegate->storiesJumpTo({}); + } + } +} + +bool Controller::jumpAvailable(int delta) const { + if (delta == -1) { + // Always allow to jump back for one. + // In case of the first story just jump to the beginning. + return _list && !_list->items.empty(); + } + const auto index = _index + delta; + return index >= 0 && index < _list->total; +} + +bool Controller::jumpFor(int delta) { + if (!_index && delta == -1) { + if (!_list || _list->items.empty()) { + return false; + } + _delegate->storiesJumpTo({ + .user = _list->user, + .id = _list->items.front().id + }); + return true; + } + const auto index = _index + delta; + if (index < 0 || index >= _list->total) { + return false; + } else if (index < _list->items.size()) { + // #TODO stories load more + _delegate->storiesJumpTo({ + .user = _list->user, + .id = _list->items[index].id + }); + } + return true; +} + +bool Controller::paused() const { + return _photoPlayback + ? _photoPlayback->paused() + : _delegate->storiesPaused(); +} + +void Controller::togglePaused(bool paused) { + if (_photoPlayback) { + _photoPlayback->togglePaused(paused); + } else { + _delegate->storiesTogglePaused(paused); + } +} + +void Controller::updatePowerSaveBlocker(const Player::TrackState &state) { + const auto block = !Player::IsPausedOrPausing(state.state) + && !Player::IsStoppedOrStopping(state.state); + base::UpdatePowerSaveBlocker( + _powerSaveBlocker, + block, + base::PowerSaveBlockType::PreventDisplaySleep, + [] { return u"Stories playback is active"_q; }, + [=] { return _wrap->window()->windowHandle(); }); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index a50ff6c8a..2d9abb80c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -7,6 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/data_stories.h" + +namespace base { +class PowerSaveBlocker; +} // namespace base + namespace ChatHelpers { class Show; struct FileChosen; @@ -20,6 +26,10 @@ namespace Ui { class RpWidget; } // namespace Ui +namespace Media::Player { +struct TrackState; +} // namespace Media::Player + namespace Media::Stories { class Header; @@ -27,17 +37,6 @@ class Slider; class ReplyArea; class Delegate; -struct ShownId { - UserData *user = nullptr; - StoryId id = 0; - - explicit operator bool() const { - return user != nullptr && id != 0; - } - friend inline auto operator<=>(ShownId, ShownId) = default; - friend inline bool operator==(ShownId, ShownId) = default; -}; - enum class HeaderLayout { Normal, Outside, @@ -59,6 +58,7 @@ struct Layout { class Controller final { public: explicit Controller(not_null delegate); + ~Controller(); [[nodiscard]] not_null wrap() const; [[nodiscard]] Layout layout() const; @@ -69,9 +69,22 @@ public: -> rpl::producer; void show(const Data::StoriesList &list, int index); + void ready(); + + void updateVideoPlayback(const Player::TrackState &state); + + [[nodiscard]] bool jumpAvailable(int delta) const; + [[nodiscard]] bool jumpFor(int delta); + [[nodiscard]] bool paused() const; + void togglePaused(bool paused); private: + class PhotoPlayback; + void initLayout(); + void updatePhotoPlayback(const Player::TrackState &state); + void updatePlayback(const Player::TrackState &state); + void updatePowerSaveBlocker(const Player::TrackState &state); const not_null _delegate; @@ -82,7 +95,12 @@ private: const std::unique_ptr _slider; const std::unique_ptr _replyArea; - ShownId _shown; + Data::FullStoryId _shown; + std::optional _list; + int _index = 0; + std::unique_ptr _photoPlayback; + + std::unique_ptr _powerSaveBlocker; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h index fd00b7cf9..3a2d700f6 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h @@ -12,12 +12,21 @@ class Show; struct FileChosen; } // namespace ChatHelpers +namespace Data { +struct FullStoryId; +} // namespace Data + namespace Ui { class RpWidget; } // namespace Ui namespace Media::Stories { +enum class JumpReason { + Finished, + User, +}; + class Delegate { public: [[nodiscard]] virtual not_null storiesWrap() = 0; @@ -25,6 +34,9 @@ public: -> std::shared_ptr = 0; [[nodiscard]] virtual auto storiesStickerOrEmojiChosen() -> rpl::producer = 0; + virtual void storiesJumpTo(Data::FullStoryId id) = 0; + [[nodiscard]] virtual bool storiesPaused() = 0; + virtual void storiesTogglePaused(bool paused) = 0; }; } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index 849e07e97..af11254a5 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -18,6 +18,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_media_view.h" namespace Media::Stories { +namespace { + +constexpr auto kNameOpacity = 1.; +constexpr auto kDateOpacity = 0.6; + +} // namespace Header::Header(not_null controller) : _controller(controller) { @@ -50,6 +56,7 @@ void Header::show(HeaderData data) { raw, data.user->firstName, st::storiesHeaderName); + name->setOpacity(kNameOpacity); name->move(st::storiesHeaderNamePosition); raw->show(); _widget = std::move(widget); @@ -63,6 +70,7 @@ void Header::show(HeaderData data) { _widget.get(), Ui::FormatDateTime(base::unixtime::parse(data.date)), st::storiesHeaderDate); + _date->setOpacity(kDateOpacity); _date->show(); _date->move(st::storiesHeaderDatePosition); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp index 2fdb53884..57f16ddd1 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_slider.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_slider.cpp @@ -8,21 +8,34 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/stories/media_stories_slider.h" #include "media/stories/media_stories_controller.h" +#include "media/view/media_view_playback_progress.h" +#include "media/audio/media_audio.h" #include "ui/painter.h" #include "ui/rp_widget.h" #include "styles/style_widgets.h" #include "styles/style_media_view.h" namespace Media::Stories { +namespace { + +constexpr auto kOpacityInactive = 0.4; +constexpr auto kOpacityActive = 1.; + +} // namespace Slider::Slider(not_null controller) -: _controller(controller) { +: _controller(controller) +, _progress(std::make_unique()) { } Slider::~Slider() { } void Slider::show(SliderData data) { + resetProgress(); + data.total = std::max(data.total, 1); + data.index = std::clamp(data.index, 0, data.total - 1); + if (_data == data) { return; } @@ -32,43 +45,101 @@ void Slider::show(SliderData data) { auto widget = std::make_unique(parent); const auto raw = widget.get(); + _rects.resize(_data.total); + + raw->widthValue() | rpl::filter([=](int width) { + return (width >= st::storiesSliderWidth); + }) | rpl::start_with_next([=](int width) { + layout(width); + }, raw->lifetime()); + raw->paintRequest( ) | rpl::filter([=] { - return (raw->width() >= st::storiesSlider.width); + return (raw->width() >= st::storiesSliderWidth); }) | rpl::start_with_next([=](QRect clip) { - auto clipf = QRectF(clip); - auto p = QPainter(raw); - const auto single = st::storiesSlider.width; - const auto skip = st::storiesSliderSkip; - // width() == single * max + skip * (max - 1); - // max == (width() + skip) / (single + skip); - const auto max = (raw->width() + skip) / (single + skip); - Assert(max > 0); - const auto count = std::clamp(_data.total, 1, max); - const auto index = std::clamp(data.index, 0, count - 1); - const auto radius = st::storiesSlider.width / 2.; - const auto width = (raw->width() - (count - 1) * skip) - / float64(count); - auto hq = PainterHighQualityEnabler(p); - auto left = 0.; - for (auto i = 0; i != count; ++i) { - const auto rect = QRectF(left, 0, width, single); - p.setBrush((i == index) // #TODO stories - ? st::mediaviewPipControlsFgOver - : st::mediaviewPipPlaybackInactive); - p.setPen(Qt::NoPen); - p.drawRoundedRect(rect, radius, radius); - left += width + skip; - } + paint(QRectF(clip)); }, raw->lifetime()); raw->show(); _widget = std::move(widget); + _progress->setValueChangedCallback([=](float64, float64) { + _widget->update(_activeBoundingRect); + }); + _controller->layoutValue( ) | rpl::start_with_next([=](const Layout &layout) { raw->setGeometry(layout.slider - st::storiesSliderMargin); }, raw->lifetime()); } +void Slider::updatePlayback(const Player::TrackState &state) { + _progress->updateState(state); +} + +void Slider::resetProgress() { + _progress->updateState({}); +} + +void Slider::layout(int width) { + const auto single = st::storiesSliderWidth; + const auto skip = st::storiesSliderSkip; + // width == single * max + skip * (max - 1); + // max == (width + skip) / (single + skip); + const auto max = (width + skip) / (single + skip); + Assert(max > 0); + const auto count = std::clamp(_data.total, 1, max); + const auto one = (width - (count - 1) * skip) / float64(count); + auto left = 0.; + for (auto i = 0; i != count; ++i) { + _rects[i] = QRectF(left, 0, one, single); + if (i == _data.index) { + const auto from = int(std::floor(left)); + const auto size = int(std::ceil(left + one)) - from; + _activeBoundingRect = QRect(from, 0, size, single); + } + left += one + skip; + } + for (auto i = count; i != _rects.size(); ++i) { + _rects[i] = QRectF(); + } +} + +void Slider::paint(QRectF clip) { + auto p = QPainter(_widget.get()); + auto hq = PainterHighQualityEnabler(p); + + p.setBrush(st::mediaviewControlFg); + p.setPen(Qt::NoPen); + const auto radius = st::storiesSliderWidth / 2.; + for (auto i = 0; i != int(_rects.size()); ++i) { + if (_rects[i].isEmpty()) { + break; + } else if (!_rects[i].intersects(clip)) { + continue; + } else if (i == _data.index) { + const auto progress = _progress->value(); + const auto full = _rects[i].width(); + const auto min = _rects[i].height(); + const auto activeWidth = std::max(full * progress, min); + const auto inactiveWidth = full - activeWidth + min; + const auto activeLeft = _rects[i].left(); + const auto inactiveLeft = activeLeft + activeWidth - min; + p.setOpacity(kOpacityInactive); + p.drawRoundedRect( + QRectF(inactiveLeft, 0, inactiveWidth, min), + radius, + radius); + p.setOpacity(kOpacityActive); + p.drawRoundedRect( + QRectF(activeLeft, 0, activeWidth, min), + radius, + radius); + } else { + p.setOpacity(kOpacityInactive); + p.drawRoundedRect(_rects[i], radius, radius); + } + } +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_slider.h b/Telegram/SourceFiles/media/stories/media_stories_slider.h index 0dd18ef08..140df471f 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_slider.h +++ b/Telegram/SourceFiles/media/stories/media_stories_slider.h @@ -11,6 +11,14 @@ namespace Ui { class RpWidget; } // namespace Ui +namespace Media::View { +class PlaybackProgress; +} // namespace Media::View + +namespace Media::Player { +struct TrackState; +} // namespace Media::Player + namespace Media::Stories { class Controller; @@ -30,13 +38,24 @@ public: void show(SliderData data); + void updatePlayback(const Player::TrackState &state); + private: + void resetProgress(); + + void layout(int width); + void paint(QRectF clip); + const not_null _controller; + const std::unique_ptr _progress; std::unique_ptr _widget; + std::vector _rects; + QRect _activeBoundingRect; SliderData _data; + }; } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index 6fcf4fd1c..cfe82693c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -25,8 +25,32 @@ void View::show(const Data::StoriesList &list, int index) { _controller->show(list, index); } +void View::ready() { + _controller->ready(); +} + QRect View::contentGeometry() const { return _controller->layout().content; } +void View::updatePlayback(const Player::TrackState &state) { + _controller->updateVideoPlayback(state); +} + +bool View::jumpAvailable(int delta) const { + return _controller->jumpAvailable(delta); +} + +bool View::jumpFor(int delta) const { + return _controller->jumpFor(delta); +} + +bool View::paused() const { + return _controller->paused(); +} + +void View::togglePaused(bool paused) { + _controller->togglePaused(paused); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index 133652e82..96f4e0042 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -11,6 +11,10 @@ namespace Data { struct StoriesList; } // namespace Data +namespace Media::Player { +struct TrackState; +} // namespace Media::Player + namespace Media::Stories { class Delegate; @@ -22,8 +26,18 @@ public: ~View(); void show(const Data::StoriesList &list, int index); + void ready(); + [[nodiscard]] QRect contentGeometry() const; + void updatePlayback(const Player::TrackState &state); + + [[nodiscard]] bool jumpAvailable(int delta) const; + [[nodiscard]] bool jumpFor(int delta) const; + + [[nodiscard]] bool paused() const; + void togglePaused(bool paused); + private: const std::unique_ptr _controller; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index fc2b6b18f..19dd46778 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -406,11 +406,8 @@ pipVolumeIcon2Over: icon {{ "player/player_volume_on", mediaviewPipControlsFgOve speedSliderDividerSize: size(2px, 8px); storiesMaxSize: size(405px, 720px); -storiesSlider: MediaSlider(mediaviewPlayback) { - width: 2px; - seekSize: size(2px, 2px); -} -storiesSliderMargin: margins(8px, 7px, 8px, 10px); +storiesSliderWidth: 2px; +storiesSliderMargin: margins(8px, 7px, 8px, 11px); storiesSliderSkip: 4px; storiesHeaderMargin: margins(12px, 3px, 12px, 8px); storiesHeaderPhoto: UserpicButton(defaultUserpicButton) { @@ -418,14 +415,14 @@ storiesHeaderPhoto: UserpicButton(defaultUserpicButton) { photoSize: 28px; } storiesHeaderName: FlatLabel(defaultFlatLabel) { - textFg: mediaviewPipControlsFgOver; // #TODO stories + textFg: mediaviewControlFg; style: semiboldTextStyle; } -storiesHeaderNamePosition: point(50px, 2px); +storiesHeaderNamePosition: point(50px, 0px); storiesHeaderDate: FlatLabel(defaultFlatLabel) { - textFg: mediaviewPipControlsFg; // #TODO stories + textFg: mediaviewControlFg; } -storiesHeaderDatePosition: point(50px, 19px); +storiesHeaderDatePosition: point(50px, 16px); storiesControlsMinWidth: 200px; storiesFieldMargin: margins(0px, 14px, 0px, 16px); storiesAttach: IconButton(defaultIconButton) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index edb309b69..13615c184 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -251,18 +251,14 @@ struct OverlayWidget::Streamed { Streamed( not_null document, Data::FileOrigin origin, - not_null controlsParent, - not_null controlsDelegate, Fn waitingCallback); Streamed( not_null photo, Data::FileOrigin origin, - not_null controlsParent, - not_null controlsDelegate, Fn waitingCallback); Streaming::Instance instance; - PlaybackControls controls; + std::unique_ptr controls; std::unique_ptr powerSaveBlocker; bool withSound = false; @@ -289,21 +285,15 @@ struct OverlayWidget::PipWrap { OverlayWidget::Streamed::Streamed( not_null document, Data::FileOrigin origin, - not_null controlsParent, - not_null controlsDelegate, Fn waitingCallback) -: instance(document, origin, std::move(waitingCallback)) -, controls(controlsParent, controlsDelegate) { +: instance(document, origin, std::move(waitingCallback)) { } OverlayWidget::Streamed::Streamed( not_null photo, Data::FileOrigin origin, - not_null controlsParent, - not_null controlsDelegate, Fn waitingCallback) -: instance(photo, origin, std::move(waitingCallback)) -, controls(controlsParent, controlsDelegate) { +: instance(photo, origin, std::move(waitingCallback)) { } OverlayWidget::PipWrap::PipWrap( @@ -542,7 +532,9 @@ OverlayWidget::OverlayWidget() Core::App().calls().currentGroupCallValue(), _1 || _2 ) | rpl::start_with_next([=](bool call) { - if (!_streamed || videoIsGifOrUserpic()) { + if (!_streamed + || !_document + || (_document->isAnimation() && !_document->isVideoMessage())) { return; } else if (call) { playbackPauseOnCall(); @@ -583,7 +575,10 @@ void OverlayWidget::setupWindow() { return Flag::None | Flag(0); } const auto inControls = (_over != OverNone) && (_over != OverVideo); - if (inControls || (_streamed && _streamed->controls.dragging())) { + if (inControls + || (_streamed + && _streamed->controls + && _streamed->controls->dragging())) { return Flag::None | Flag(0); } else if ((_w > _widget->width() || _h > _widget->height()) && (widgetPoint.y() > st::mediaviewHeaderTop) @@ -881,10 +876,10 @@ QSize OverlayWidget::videoSize() const { return flipSizeByRotation(_streamed->instance.info().video.size); } -bool OverlayWidget::videoIsGifOrUserpic() const { - return _streamed - && (!_document - || (_document->isAnimation() && !_document->isVideoMessage())); +bool OverlayWidget::streamingRequiresControls() const { + return !_stories + && _document + && (!_document->isAnimation() || _document->isVideoMessage()); } QImage OverlayWidget::videoFrame() const { @@ -979,13 +974,13 @@ void OverlayWidget::documentUpdated(not_null document) { updateDocSize(); _widget->update(_docRect); } - } else if (_streamed) { + } else if (_streamed && _streamed->controls) { const auto ready = _documentMedia->loaded() ? _document->size : _document->loading() ? std::clamp(_document->loadOffset(), int64(), _document->size) : 0; - _streamed->controls.setLoadingProgress(ready, _document->size); + _streamed->controls->setLoadingProgress(ready, _document->size); } } @@ -1013,7 +1008,10 @@ void OverlayWidget::updateDocSize() { } void OverlayWidget::refreshNavVisibility() { - if (_sharedMediaData) { + if (_stories) { + _leftNavVisible = _stories->jumpAvailable(-1); + _rightNavVisible = _stories->jumpAvailable(1); + } else if (_sharedMediaData) { _leftNavVisible = _index && (*_index > 0); _rightNavVisible = _index && (*_index + 1 < _sharedMediaData->size()); } else if (_userPhotosData) { @@ -1029,7 +1027,7 @@ void OverlayWidget::refreshNavVisibility() { } bool OverlayWidget::contentCanBeSaved() const { - if (hasCopyMediaRestriction()) { + if (_stories || hasCopyMediaRestriction()) { return false; } else if (_photo) { return _photo->hasVideo() || _photoMedia->loaded(); @@ -1108,7 +1106,7 @@ void OverlayWidget::updateControls() { QPoint(), QSize(st::mediaviewIconOver, st::mediaviewIconOver)); _saveVisible = contentCanBeSaved(); - _rotateVisible = !_themePreviewShown; + _rotateVisible = !_themePreviewShown && !_stories; const auto navRect = [&](int i) { return QRect(width() - st::mediaviewIconSize.width() * i, height() - st::mediaviewIconSize.height(), @@ -1181,8 +1179,8 @@ void OverlayWidget::refreshCaptionGeometry() { _groupThumbs = nullptr; _groupThumbsRect = QRect(); } - const auto captionBottom = (_streamed && !videoIsGifOrUserpic()) - ? (_streamed->controls.y() - st::mediaviewCaptionMargin.height()) + const auto captionBottom = (_streamed && _streamed->controls) + ? (_streamed->controls->y() - st::mediaviewCaptionMargin.height()) : _groupThumbs ? _groupThumbsTop : height() - st::mediaviewCaptionMargin.height(); @@ -1523,9 +1521,9 @@ void OverlayWidget::contentSizeChanged() { } void OverlayWidget::recountSkipTop() { - const auto bottom = (!_streamed || videoIsGifOrUserpic()) + const auto bottom = (!_streamed || !_streamed->controls) ? height() - : (_streamed->controls.y() - st::mediaviewCaptionPadding.bottom()); + : (_streamed->controls->y() - st::mediaviewCaptionPadding.bottom()); const auto skipHeightBottom = (height() - bottom); _skipTop = std::min( std::max( @@ -1869,12 +1867,12 @@ void OverlayWidget::toggleFullScreen(bool fullscreen) { } void OverlayWidget::activateControls() { - if (!_menu && !_mousePressed) { + if (!_menu && !_mousePressed && !_stories) { _controlsHideTimer.callOnce(st::mediaviewWaitHide); } if (_fullScreenVideo) { - if (_streamed) { - _streamed->controls.showAnimated(); + if (_streamed && _streamed->controls) { + _streamed->controls->showAnimated(); } } if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) { @@ -1888,16 +1886,23 @@ void OverlayWidget::activateControls() { } void OverlayWidget::hideControls(bool force) { - if (!force) { + if (_stories) { + _controlsState = ControlsShown; + _controlsOpacity = anim::value(1); + _helper->setControlsOpacity(1.); + return; + } else if (!force) { if (!_dropdown->isHidden() - || (_streamed && _streamed->controls.hasMenu()) + || (_streamed + && _streamed->controls + && _streamed->controls->hasMenu()) || _menu || _mousePressed) { return; } } - if (_fullScreenVideo) { - _streamed->controls.hideAnimated(); + if (_fullScreenVideo && _streamed && _streamed->controls) { + _streamed->controls->hideAnimated(); } if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) return; @@ -2959,7 +2964,7 @@ void OverlayWidget::displayPhoto(not_null photo) { refreshMediaViewer(); _staticContent = QImage(); - if (_photo->videoCanBePlayed()) { + if (!_stories && _photo->videoCanBePlayed()) { initStreaming(); } @@ -3133,7 +3138,7 @@ void OverlayWidget::displayDocument( } refreshFromLabel(); _blurred = false; - if (_showAsPip && _streamed && !videoIsGifOrUserpic()) { + if (_showAsPip && _streamed && _streamed->controls) { switchToPip(); } else { displayFinished(); @@ -3348,20 +3353,12 @@ void OverlayWidget::applyVideoSize() { bool OverlayWidget::createStreamingObjects() { Expects(_photo || _document); + const auto origin = fileOrigin(); + const auto callback = [=] { waitingAnimationCallback(); }; if (_document) { - _streamed = std::make_unique( - _document, - fileOrigin(), - _body, - static_cast(this), - [=] { waitingAnimationCallback(); }); + _streamed = std::make_unique(_document, origin, callback); } else { - _streamed = std::make_unique( - _photo, - fileOrigin(), - _body, - static_cast(this), - [=] { waitingAnimationCallback(); }); + _streamed = std::make_unique(_photo, origin, callback); } if (!_streamed->instance.valid()) { _streamed = nullptr; @@ -3375,12 +3372,12 @@ bool OverlayWidget::createStreamingObjects() { || _document->isVideoFile() || _document->isVoiceMessage() || _document->isVideoMessage()); - - if (videoIsGifOrUserpic()) { - _streamed->controls.hide(); - } else { + if (streamingRequiresControls()) { + _streamed->controls = std::make_unique( + _body, + static_cast(this)); + _streamed->controls->show(); refreshClipControllerGeometry(); - _streamed->controls.show(); } return true; } @@ -3569,7 +3566,7 @@ void OverlayWidget::initThemePreview() { } void OverlayWidget::refreshClipControllerGeometry() { - if (!_streamed || videoIsGifOrUserpic()) { + if (!_streamed || !_streamed->controls) { return; } @@ -3584,13 +3581,15 @@ void OverlayWidget::refreshClipControllerGeometry() { const auto controllerWidth = std::min( st::mediaviewControllerSize.width(), width() - 2 * skip); - _streamed->controls.resize( + _streamed->controls->resize( controllerWidth, st::mediaviewControllerSize.height()); - _streamed->controls.move( + _streamed->controls->move( (width() - controllerWidth) / 2, - controllerBottom - _streamed->controls.height() - st::mediaviewCaptionPadding.bottom()); - Ui::SendPendingMoveResizeEvents(&_streamed->controls); + (controllerBottom + - _streamed->controls->height() + - st::mediaviewCaptionPadding.bottom())); + Ui::SendPendingMoveResizeEvents(_streamed->controls.get()); } void OverlayWidget::playbackControlsPlay() { @@ -3614,7 +3613,7 @@ void OverlayWidget::playbackControlsFromFullScreen() { } void OverlayWidget::playbackControlsToPictureInPicture() { - if (!videoIsGifOrUserpic()) { + if (_streamed && _streamed->controls) { switchToPip(); } } @@ -3775,7 +3774,7 @@ void OverlayWidget::playbackControlsSpeedChanged(float64 speed) { Core::App().settings().setVideoPlaybackSpeed(speed); Core::App().saveSettingsDelayed(); } - if (_streamed && !videoIsGifOrUserpic()) { + if (_streamed && _streamed->controls) { DEBUG_LOG(("Media playback speed: %1 to _streamed.").arg(speed)); _streamed->instance.setSpeed(speed); } @@ -3921,10 +3920,66 @@ auto OverlayWidget::storiesStickerOrEmojiChosen() return _storiesStickerOrEmojiChosen.events(); } +void OverlayWidget::storiesJumpTo(Data::FullStoryId id) { + Expects(_stories != nullptr); + + if (!id) { + close(); + return; + } + const auto &all = id.user->owner().stories().all(); + const auto i = ranges::find( + all, + not_null(id.user), + &Data::StoriesList::user); + if (i == end(all)) { + close(); + return; + } + const auto j = ranges::find(i->items, id.id, &Data::StoryItem::id); + if (j == end(i->items)) { + close(); + return; + } + setContext(StoriesContext{ i->user, id.id }); + clearStreaming(); + _streamingStartPaused = false; + const auto &data = j->media.data; + if (const auto photo = std::get_if>(&data)) { + displayPhoto(*photo); + } else { + displayDocument(v::get>(data)); + } +} + +bool OverlayWidget::storiesPaused() { + return _streamed + && !_streamed->instance.player().failed() + && !_streamed->instance.player().finished() + && _streamed->instance.player().active() + && _streamed->instance.player().paused(); +} + +void OverlayWidget::storiesTogglePaused(bool paused) { + if (!_streamed + || _streamed->instance.player().failed() + || _streamed->instance.player().finished() + || !_streamed->instance.player().active()) { + return; + } else if (_streamed->instance.player().paused()) { + _streamed->instance.resume(); + updatePlaybackState(); + playbackPauseMusic(); + } else { + _streamed->instance.pause(); + updatePlaybackState(); + } +} + void OverlayWidget::playbackToggleFullScreen() { Expects(_streamed != nullptr); - if (!videoShown() || (videoIsGifOrUserpic() && !_fullScreenVideo)) { + if (!videoShown() || (!_streamed->controls && !_fullScreenVideo)) { return; } _fullScreenVideo = !_fullScreenVideo; @@ -3936,10 +3991,12 @@ void OverlayWidget::playbackToggleFullScreen() { setZoomLevel( _fullScreenVideo ? kZoomToScreenLevel : _fullScreenZoomCache, true); - if (!_fullScreenVideo) { - _streamed->controls.showAnimated(); + if (_streamed->controls) { + if (!_fullScreenVideo) { + _streamed->controls->showAnimated(); + } + _streamed->controls->setInFullScreen(_fullScreenVideo); } - _streamed->controls.setInFullScreen(_fullScreenVideo); _touchbarFullscreenToggled.fire_copy(_fullScreenVideo); updateControls(); update(); @@ -3981,14 +4038,19 @@ void OverlayWidget::playbackPauseMusic() { void OverlayWidget::updatePlaybackState() { Expects(_streamed != nullptr); - if (videoIsGifOrUserpic()) { + if (!_streamed->controls && !_stories) { return; } const auto state = _streamed->instance.player().prepareLegacyState(); if (state.position != kTimeUnknown && state.length != kTimeUnknown) { - _streamed->controls.updatePlayback(state); - updatePowerSaveBlocker(state); - _touchbarTrackState.fire_copy(state); + if (_streamed->controls) { + _streamed->controls->updatePlayback(state); + _touchbarTrackState.fire_copy(state); + updatePowerSaveBlocker(state); + } + if (_stories) { + _stories->updatePlayback(state); + } } } @@ -4050,9 +4112,15 @@ void OverlayWidget::paint(not_null renderer) { renderer->paintTransformedVideoFrame(contentGeometry()); if (_streamed->instance.player().ready()) { _streamed->instance.markFrameShown(); + if (_stories) { + _stories->ready(); + } } } else { validatePhotoCurrentImage(); + if (_stories && !_blurred) { + _stories->ready(); + } const auto fillTransparentBackground = (!_document || (!_document->sticker() && !_document->isVideoMessage())) && _staticContentTransparent; @@ -4077,7 +4145,9 @@ void OverlayWidget::paint(not_null renderer) { const auto opacity = _fullScreenVideo ? 0. : _controlsOpacity.current(); if (opacity > 0) { paintControls(renderer, opacity); - renderer->paintFooter(footerGeometry(), opacity); + if (!_stories) { + renderer->paintFooter(footerGeometry(), opacity); + } if (!_caption.isEmpty()) { renderer->paintCaption(captionGeometry(), opacity); } @@ -4510,6 +4580,10 @@ void OverlayWidget::handleKeyPress(not_null e) { const auto key = e->key(); const auto modifiers = e->modifiers(); const auto ctrl = modifiers.testFlag(Qt::ControlModifier); + if (_stories && key == Qt::Key_Space && _down != OverVideo) { + _stories->togglePaused(!_stories->paused()); + return; + } if (_streamed) { // Ctrl + F for full screen toggle is in eventFilter(). const auto toggleFull = (modifiers.testFlag(Qt::AltModifier) || ctrl) @@ -4833,7 +4907,9 @@ void OverlayWidget::setSession(not_null session) { } bool OverlayWidget::moveToNext(int delta) { - if (!_index) { + if (_stories) { + return _stories->jumpFor(delta); + } else if (!_index) { return false; } auto newIndex = *_index + delta; @@ -4928,6 +5004,9 @@ void OverlayWidget::handleMousePress( || _over == OverMore || _over == OverVideo) { _down = _over; + if (_over == OverVideo && _stories) { + _stories->togglePaused(true); + } } else if (!_saveMsg.contains(position) || !isSaveMsgShown()) { _pressed = true; _dragging = 0; @@ -4950,9 +5029,12 @@ bool OverlayWidget::handleDoubleClick( if (_over != OverVideo || !_streamed || button != Qt::LeftButton) { return false; + } else if (_stories) { + toggleFullScreen(_windowed); + } else { + playbackToggleFullScreen(); + playbackPauseResume(); } - playbackToggleFullScreen(); - playbackPauseResume(); return true; } @@ -5090,11 +5172,11 @@ void OverlayWidget::updateOver(QPoint pos) { updateOverState(OverLeftNav); } else if (_rightNavVisible && _rightNav.contains(pos)) { updateOverState(OverRightNav); - } else if (_from && _nameNav.contains(pos)) { + } else if (!_stories && _from && _nameNav.contains(pos)) { updateOverState(OverName); - } else if (_message && _message->isRegular() && _dateNav.contains(pos)) { + } else if (!_stories && _message && _message->isRegular() && _dateNav.contains(pos)) { updateOverState(OverDate); - } else if (_headerHasLink && _headerNav.contains(pos)) { + } else if (!_stories && _headerHasLink && _headerNav.contains(pos)) { updateOverState(OverHeader); } else if (_saveVisible && _saveNav.contains(pos)) { updateOverState(OverSave); @@ -5104,10 +5186,14 @@ void OverlayWidget::updateOver(QPoint pos) { updateOverState(OverIcon); } else if (_moreNav.contains(pos)) { updateOverState(OverMore); - } else if (documentContentShown() && finalContentRect().contains(pos)) { - if ((_document->isVideoFile() || _document->isVideoMessage()) && _streamed) { + } else if (contentShown() && finalContentRect().contains(pos)) { + if (_stories) { updateOverState(OverVideo); - } else if (!_streamed && !_documentMedia->loaded()) { + } else if (_streamed + && _document + && (_document->isVideoFile() || _document->isVideoMessage())) { + updateOverState(OverVideo); + } else if (!_streamed && _document && !_documentMedia->loaded()) { updateOverState(OverIcon); } else if (_over != OverNone) { updateOverState(OverNone); @@ -5163,7 +5249,9 @@ void OverlayWidget::handleMouseRelease( } else if (_over == OverMore && _down == OverMore) { InvokeQueued(_widget, [=] { showDropdown(); }); } else if (_over == OverVideo && _down == OverVideo) { - if (_streamed) { + if (_stories) { + _stories->togglePaused(false); + } else if (_streamed) { playbackPauseResume(); } } else if (_pressed) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 4d6d6d3f2..ad7506ee1 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -26,6 +26,7 @@ class History; namespace Data { class PhotoMedia; class DocumentMedia; +struct FullStoryId; } // namespace Data namespace Ui { @@ -227,6 +228,9 @@ private: std::shared_ptr storiesShow() override; auto storiesStickerOrEmojiChosen() -> rpl::producer override; + void storiesJumpTo(Data::FullStoryId id) override; + bool storiesPaused() override; + void storiesTogglePaused(bool paused) override; void hideControls(bool force = false); void subscribeToScreenGeometry(); @@ -458,7 +462,7 @@ private: void applyVideoSize(); [[nodiscard]] bool videoShown() const; [[nodiscard]] QSize videoSize() const; - [[nodiscard]] bool videoIsGifOrUserpic() const; + [[nodiscard]] bool streamingRequiresControls() const; [[nodiscard]] QImage videoFrame() const; // ARGB (changes prepare format) [[nodiscard]] QImage currentVideoFrameImage() const; // RGB (may convert) [[nodiscard]] Streaming::FrameWithInfo videoFrameWithInfo() const; // YUV