/* 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 "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) , _wrap(_delegate->storiesWrap()) , _header(std::make_unique
(this)) , _slider(std::make_unique(this)) , _replyArea(std::make_unique(this)) { 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::storiesSliderWidth + st::storiesSliderMargin.bottom(); const auto outsideHeaderHeight = headerHeight + sliderHeight; const auto fieldMinHeight = st::storiesFieldMargin.top() + st::storiesAttach.height + st::storiesFieldMargin.bottom(); const auto minHeightForOutsideHeader = st::storiesMaxSize.height() + outsideHeaderHeight + fieldMinHeight; _layout = _wrap->sizeValue( ) | rpl::map([=](QSize size) { size = QSize( std::max(size.width(), st::mediaviewMinWidth), std::max(size.height(), st::mediaviewMinHeight)); auto layout = Layout(); layout.headerLayout = (size.height() >= minHeightForOutsideHeader) ? HeaderLayout::Outside : HeaderLayout::Normal; const auto topSkip = (layout.headerLayout == HeaderLayout::Outside) ? outsideHeaderHeight : st::storiesFieldMargin.bottom(); const auto bottomSkip = fieldMinHeight; const auto maxWidth = size.width() - 2 * st::storiesSideSkip; const auto availableHeight = size.height() - topSkip - bottomSkip; const auto maxContentHeight = std::min( availableHeight, st::storiesMaxSize.height()); const auto nowWidth = maxContentHeight * st::storiesMaxSize.width() / st::storiesMaxSize.height(); const auto contentWidth = std::min(nowWidth, maxWidth); const auto contentHeight = (contentWidth < nowWidth) ? (contentWidth * st::storiesMaxSize.height() / st::storiesMaxSize.width()) : maxContentHeight; const auto addedTopSkip = (availableHeight - contentHeight) / 2; layout.content = QRect( (size.width() - contentWidth) / 2, addedTopSkip + topSkip, contentWidth, contentHeight); if (layout.headerLayout == HeaderLayout::Outside) { layout.header = QRect( layout.content.topLeft() - QPoint(0, outsideHeaderHeight), QSize(contentWidth, outsideHeaderHeight)); layout.slider = QRect( layout.header.topLeft() + QPoint(0, headerHeight), QSize(contentWidth, sliderHeight)); } else { layout.slider = QRect( layout.content.topLeft(), QSize(contentWidth, sliderHeight)); layout.header = QRect( layout.slider.topLeft() + QPoint(0, sliderHeight), QSize(contentWidth, headerHeight)); } layout.controlsWidth = std::max( layout.content.width(), st::storiesControlsMinWidth); layout.controlsBottomPosition = QPoint( (size.width() - layout.controlsWidth) / 2, (layout.content.y() + layout.content.height() + fieldMinHeight - st::storiesFieldMargin.bottom())); layout.autocompleteRect = QRect( layout.controlsBottomPosition.x(), 0, layout.controlsWidth, layout.controlsBottomPosition.y()); return layout; }); } not_null Controller::wrap() const { return _wrap; } Layout Controller::layout() const { Expects(_layout.current().has_value()); return *_layout.current(); } rpl::producer Controller::layoutValue() const { return _layout.value() | rpl::filter_optional(); } std::shared_ptr Controller::uiShow() const { return _delegate->storiesShow(); } auto Controller::stickerOrEmojiChosen() const ->rpl::producer { return _delegate->storiesStickerOrEmojiChosen(); } 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 = Data::FullStoryId{ .user = list.user, .id = item.id, }; if (_shown == id) { return; } _shown = id; _header->show({ .user = list.user, .date = item.date }); _slider->show({ .index = index, .total = int(list.items.size()) }); _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