Implement stories switching, photo "animation".

This commit is contained in:
John Preston 2023-05-07 00:20:21 +04:00
parent 027bd89e5b
commit 7717de19ab
13 changed files with 581 additions and 131 deletions

View File

@ -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);

View File

@ -15,10 +15,13 @@ namespace Data {
class Session;
struct StoryPrivacy {
friend inline bool operator==(StoryPrivacy, StoryPrivacy) = default;
};
struct StoryMedia {
std::variant<not_null<PhotoData*>, not_null<DocumentData*>> 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<UserData*> user;
std::vector<StoryItem> 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 {

View File

@ -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*> controller);
[[nodiscard]] bool paused() const;
void togglePaused(bool paused);
private:
void callback();
const not_null<Controller*> _controller;
base::Timer _timer;
crl::time _started = 0;
crl::time _paused = 0;
};
Controller::PhotoPlayback::PhotoPlayback(not_null<Controller*> 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(delegate)
@ -28,12 +106,14 @@ Controller::Controller(not_null<Delegate*> 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<not_null<PhotoData*>>(item.media.data)) {
_photoPlayback = std::make_unique<PhotoPlayback>(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

View File

@ -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*> delegate);
~Controller();
[[nodiscard]] not_null<Ui::RpWidget*> wrap() const;
[[nodiscard]] Layout layout() const;
@ -69,9 +69,22 @@ public:
-> rpl::producer<ChatHelpers::FileChosen>;
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*> _delegate;
@ -82,7 +95,12 @@ private:
const std::unique_ptr<Slider> _slider;
const std::unique_ptr<ReplyArea> _replyArea;
ShownId _shown;
Data::FullStoryId _shown;
std::optional<Data::StoriesList> _list;
int _index = 0;
std::unique_ptr<PhotoPlayback> _photoPlayback;
std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
rpl::lifetime _lifetime;

View File

@ -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<Ui::RpWidget*> storiesWrap() = 0;
@ -25,6 +34,9 @@ public:
-> std::shared_ptr<ChatHelpers::Show> = 0;
[[nodiscard]] virtual auto storiesStickerOrEmojiChosen()
-> rpl::producer<ChatHelpers::FileChosen> = 0;
virtual void storiesJumpTo(Data::FullStoryId id) = 0;
[[nodiscard]] virtual bool storiesPaused() = 0;
virtual void storiesTogglePaused(bool paused) = 0;
};
} // namespace Media::Stories

View File

@ -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(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);
}

View File

@ -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(controller)
, _progress(std::make_unique<View::PlaybackProgress>()) {
}
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<Ui::RpWidget>(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

View File

@ -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*> _controller;
const std::unique_ptr<Media::View::PlaybackProgress> _progress;
std::unique_ptr<Ui::RpWidget> _widget;
std::vector<QRectF> _rects;
QRect _activeBoundingRect;
SliderData _data;
};
} // namespace Media::Stories

View File

@ -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

View File

@ -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> _controller;

View File

@ -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) {

View File

@ -251,18 +251,14 @@ struct OverlayWidget::Streamed {
Streamed(
not_null<DocumentData*> document,
Data::FileOrigin origin,
not_null<QWidget*> controlsParent,
not_null<PlaybackControls::Delegate*> controlsDelegate,
Fn<void()> waitingCallback);
Streamed(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
not_null<QWidget*> controlsParent,
not_null<PlaybackControls::Delegate*> controlsDelegate,
Fn<void()> waitingCallback);
Streaming::Instance instance;
PlaybackControls controls;
std::unique_ptr<PlaybackControls> controls;
std::unique_ptr<base::PowerSaveBlocker> powerSaveBlocker;
bool withSound = false;
@ -289,21 +285,15 @@ struct OverlayWidget::PipWrap {
OverlayWidget::Streamed::Streamed(
not_null<DocumentData*> document,
Data::FileOrigin origin,
not_null<QWidget*> controlsParent,
not_null<PlaybackControls::Delegate*> controlsDelegate,
Fn<void()> waitingCallback)
: instance(document, origin, std::move(waitingCallback))
, controls(controlsParent, controlsDelegate) {
: instance(document, origin, std::move(waitingCallback)) {
}
OverlayWidget::Streamed::Streamed(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
not_null<QWidget*> controlsParent,
not_null<PlaybackControls::Delegate*> controlsDelegate,
Fn<void()> 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<DocumentData*> 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<PhotoData*> 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<Streamed>(
_document,
fileOrigin(),
_body,
static_cast<PlaybackControls::Delegate*>(this),
[=] { waitingAnimationCallback(); });
_streamed = std::make_unique<Streamed>(_document, origin, callback);
} else {
_streamed = std::make_unique<Streamed>(
_photo,
fileOrigin(),
_body,
static_cast<PlaybackControls::Delegate*>(this),
[=] { waitingAnimationCallback(); });
_streamed = std::make_unique<Streamed>(_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<PlaybackControls>(
_body,
static_cast<PlaybackControls::Delegate*>(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<not_null<PhotoData*>>(&data)) {
displayPhoto(*photo);
} else {
displayDocument(v::get<not_null<DocumentData*>>(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) {
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*> 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<QKeyEvent*> 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<Main::Session*> 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) {

View File

@ -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<ChatHelpers::Show> storiesShow() override;
auto storiesStickerOrEmojiChosen()
-> rpl::producer<ChatHelpers::FileChosen> 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