mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +02:00
Detach SystemMediaControls from Window::Controller.
This commit is contained in:
parent
6b8f80bd63
commit
cdfdccbb66
12 changed files with 67 additions and 53 deletions
|
@ -65,6 +65,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "media/player/media_player_instance.h"
|
#include "media/player/media_player_instance.h"
|
||||||
#include "media/player/media_player_float.h"
|
#include "media/player/media_player_float.h"
|
||||||
#include "media/clip/media_clip_reader.h" // For Media::Clip::Finish().
|
#include "media/clip/media_clip_reader.h" // For Media::Clip::Finish().
|
||||||
|
#include "media/system_media_controls_manager.h"
|
||||||
#include "window/notifications_manager.h"
|
#include "window/notifications_manager.h"
|
||||||
#include "window/themes/window_theme.h"
|
#include "window/themes/window_theme.h"
|
||||||
#include "window/window_lock_widgets.h"
|
#include "window/window_lock_widgets.h"
|
||||||
|
@ -147,6 +148,9 @@ Application::Application(not_null<Launcher*> launcher)
|
||||||
, _audio(std::make_unique<Media::Audio::Instance>())
|
, _audio(std::make_unique<Media::Audio::Instance>())
|
||||||
, _fallbackProductionConfig(
|
, _fallbackProductionConfig(
|
||||||
std::make_unique<MTP::Config>(MTP::Environment::Production))
|
std::make_unique<MTP::Config>(MTP::Environment::Production))
|
||||||
|
, _mediaControlsManager(MediaControlsManager::Supported()
|
||||||
|
? std::make_unique<MediaControlsManager>()
|
||||||
|
: nullptr)
|
||||||
, _downloadManager(std::make_unique<Data::DownloadManager>())
|
, _downloadManager(std::make_unique<Data::DownloadManager>())
|
||||||
, _domain(std::make_unique<Main::Domain>(cDataFile()))
|
, _domain(std::make_unique<Main::Domain>(cDataFile()))
|
||||||
, _exportManager(std::make_unique<Export::Manager>())
|
, _exportManager(std::make_unique<Export::Manager>())
|
||||||
|
@ -484,27 +488,33 @@ void Application::startTray() {
|
||||||
|
|
||||||
_tray->showFromTrayRequests(
|
_tray->showFromTrayRequests(
|
||||||
) | rpl::start_with_next([=] {
|
) | rpl::start_with_next([=] {
|
||||||
const auto last = _lastActiveWindow;
|
activate();
|
||||||
const auto primary = _lastActivePrimaryWindow;
|
|
||||||
enumerateWindows([&](WindowRaw w) {
|
|
||||||
if (w != last && w != primary) {
|
|
||||||
w->widget()->showFromTray();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (primary) {
|
|
||||||
primary->widget()->showFromTray();
|
|
||||||
}
|
|
||||||
if (last && last != primary) {
|
|
||||||
last->widget()->showFromTray();
|
|
||||||
}
|
|
||||||
}, _lifetime);
|
}, _lifetime);
|
||||||
|
|
||||||
_tray->hideToTrayRequests(
|
_tray->hideToTrayRequests(
|
||||||
) | rpl::start_with_next([=] {
|
) | rpl::start_with_next([=] {
|
||||||
enumerateWindows([&](WindowRaw w) { w->widget()->minimizeToTray(); });
|
enumerateWindows([&](WindowRaw w) {
|
||||||
|
w->widget()->minimizeToTray();
|
||||||
|
});
|
||||||
}, _lifetime);
|
}, _lifetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Application::activate() {
|
||||||
|
const auto last = _lastActiveWindow;
|
||||||
|
const auto primary = _lastActivePrimaryWindow;
|
||||||
|
enumerateWindows([&](not_null<Window::Controller*> w) {
|
||||||
|
if (w != last && w != primary) {
|
||||||
|
w->widget()->showFromTray();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (primary) {
|
||||||
|
primary->widget()->showFromTray();
|
||||||
|
}
|
||||||
|
if (last && last != primary) {
|
||||||
|
last->widget()->showFromTray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto Application::prepareEmojiSourceImages()
|
auto Application::prepareEmojiSourceImages()
|
||||||
-> std::shared_ptr<Ui::Emoji::UniversalImages> {
|
-> std::shared_ptr<Ui::Emoji::UniversalImages> {
|
||||||
const auto &images = Ui::Emoji::SourceImages();
|
const auto &images = Ui::Emoji::SourceImages();
|
||||||
|
|
|
@ -71,6 +71,7 @@ namespace Player {
|
||||||
class FloatController;
|
class FloatController;
|
||||||
class FloatDelegate;
|
class FloatDelegate;
|
||||||
} // namespace Player
|
} // namespace Player
|
||||||
|
class SystemMediaControlsManager;
|
||||||
} // namespace Media
|
} // namespace Media
|
||||||
|
|
||||||
namespace Lang {
|
namespace Lang {
|
||||||
|
@ -180,6 +181,7 @@ public:
|
||||||
void checkSystemDarkMode();
|
void checkSystemDarkMode();
|
||||||
[[nodiscard]] bool isActiveForTrayMenu() const;
|
[[nodiscard]] bool isActiveForTrayMenu() const;
|
||||||
void closeChatFromWindows(not_null<PeerData*> peer);
|
void closeChatFromWindows(not_null<PeerData*> peer);
|
||||||
|
void activate();
|
||||||
|
|
||||||
// Media view interface.
|
// Media view interface.
|
||||||
bool hideMediaView();
|
bool hideMediaView();
|
||||||
|
@ -384,6 +386,8 @@ private:
|
||||||
// Mutable because is created in run() after OpenSSL is inited.
|
// Mutable because is created in run() after OpenSSL is inited.
|
||||||
std::unique_ptr<Window::Notifications::System> _notifications;
|
std::unique_ptr<Window::Notifications::System> _notifications;
|
||||||
|
|
||||||
|
using MediaControlsManager = Media::SystemMediaControlsManager;
|
||||||
|
const std::unique_ptr<MediaControlsManager> _mediaControlsManager;
|
||||||
const std::unique_ptr<Data::DownloadManager> _downloadManager;
|
const std::unique_ptr<Data::DownloadManager> _downloadManager;
|
||||||
const std::unique_ptr<Main::Domain> _domain;
|
const std::unique_ptr<Main::Domain> _domain;
|
||||||
const std::unique_ptr<Export::Manager> _exportManager;
|
const std::unique_ptr<Export::Manager> _exportManager;
|
||||||
|
|
|
@ -287,6 +287,11 @@ MainWidget::MainWidget(
|
||||||
_exportTopBar->finishAnimating();
|
_exportTopBar->finishAnimating();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Media::Player::instance()->closePlayerRequests(
|
||||||
|
) | rpl::start_with_next([=] {
|
||||||
|
closeBothPlayers();
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
Media::Player::instance()->updatedNotifier(
|
Media::Player::instance()->updatedNotifier(
|
||||||
) | rpl::start_with_next([=](const Media::Player::TrackState &state) {
|
) | rpl::start_with_next([=](const Media::Player::TrackState &state) {
|
||||||
handleAudioUpdate(state);
|
handleAudioUpdate(state);
|
||||||
|
@ -357,14 +362,16 @@ MainWidget::MainWidget(
|
||||||
Media::Player::instance()->tracksFinished(
|
Media::Player::instance()->tracksFinished(
|
||||||
) | rpl::start_with_next([=](AudioMsgId::Type type) {
|
) | rpl::start_with_next([=](AudioMsgId::Type type) {
|
||||||
if (type == AudioMsgId::Type::Voice) {
|
if (type == AudioMsgId::Type::Voice) {
|
||||||
const auto songState = Media::Player::instance()->getState(AudioMsgId::Type::Song);
|
const auto songState = Media::Player::instance()->getState(
|
||||||
|
AudioMsgId::Type::Song);
|
||||||
if (!songState.id || IsStoppedOrStopping(songState.state)) {
|
if (!songState.id || IsStoppedOrStopping(songState.state)) {
|
||||||
closeBothPlayers();
|
Media::Player::instance()->stopAndClose();
|
||||||
}
|
}
|
||||||
} else if (type == AudioMsgId::Type::Song) {
|
} else if (type == AudioMsgId::Type::Song) {
|
||||||
const auto songState = Media::Player::instance()->getState(AudioMsgId::Type::Song);
|
const auto songState = Media::Player::instance()->getState(
|
||||||
|
AudioMsgId::Type::Song);
|
||||||
if (!songState.id) {
|
if (!songState.id) {
|
||||||
closeBothPlayers();
|
Media::Player::instance()->stopAndClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, lifetime());
|
}, lifetime());
|
||||||
|
@ -767,7 +774,7 @@ void MainWidget::handleAudioUpdate(const Media::Player::TrackState &state) {
|
||||||
if (!Media::Player::IsStoppedOrStopping(state.state)) {
|
if (!Media::Player::IsStoppedOrStopping(state.state)) {
|
||||||
createPlayer();
|
createPlayer();
|
||||||
} else if (state.state == State::StoppedAtStart) {
|
} else if (state.state == State::StoppedAtStart) {
|
||||||
closeBothPlayers();
|
Media::Player::instance()->stopAndClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const auto item = session().data().message(state.id.contextId())) {
|
if (const auto item = session().data().message(state.id.contextId())) {
|
||||||
|
@ -788,12 +795,7 @@ void MainWidget::closeBothPlayers() {
|
||||||
if (_player) {
|
if (_player) {
|
||||||
_player->hide(anim::type::normal);
|
_player->hide(anim::type::normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
_playerPlaylist->hideIgnoringEnterEvents();
|
_playerPlaylist->hideIgnoringEnterEvents();
|
||||||
Media::Player::instance()->stop(AudioMsgId::Type::Voice);
|
|
||||||
Media::Player::instance()->stop(AudioMsgId::Type::Song);
|
|
||||||
|
|
||||||
Shortcuts::ToggleMediaShortcuts(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWidget::stopAndClosePlayer() {
|
void MainWidget::stopAndClosePlayer() {
|
||||||
|
@ -814,7 +816,9 @@ void MainWidget::createPlayer() {
|
||||||
) | rpl::start_with_next(
|
) | rpl::start_with_next(
|
||||||
[this] { playerHeightUpdated(); },
|
[this] { playerHeightUpdated(); },
|
||||||
_player->lifetime());
|
_player->lifetime());
|
||||||
_player->entity()->setCloseCallback([=] { closeBothPlayers(); });
|
_player->entity()->setCloseCallback([=] {
|
||||||
|
Media::Player::instance()->stopAndClose();
|
||||||
|
});
|
||||||
_player->entity()->setShowItemCallback([=](
|
_player->entity()->setShowItemCallback([=](
|
||||||
not_null<const HistoryItem*> item) {
|
not_null<const HistoryItem*> item) {
|
||||||
_controller->showMessage(item);
|
_controller->showMessage(item);
|
||||||
|
|
|
@ -233,7 +233,6 @@ public:
|
||||||
|
|
||||||
using FloatDelegate::floatPlayerAreaUpdated;
|
using FloatDelegate::floatPlayerAreaUpdated;
|
||||||
|
|
||||||
void closeBothPlayers();
|
|
||||||
void stopAndClosePlayer();
|
void stopAndClosePlayer();
|
||||||
|
|
||||||
bool preventsCloseSection(Fn<void()> callback) const;
|
bool preventsCloseSection(Fn<void()> callback) const;
|
||||||
|
@ -302,6 +301,8 @@ private:
|
||||||
void hiderLayer(base::unique_qptr<Window::HistoryHider> h);
|
void hiderLayer(base::unique_qptr<Window::HistoryHider> h);
|
||||||
void clearHider(not_null<Window::HistoryHider*> instance);
|
void clearHider(not_null<Window::HistoryHider*> instance);
|
||||||
|
|
||||||
|
void closeBothPlayers();
|
||||||
|
|
||||||
[[nodiscard]] auto floatPlayerDelegate()
|
[[nodiscard]] auto floatPlayerDelegate()
|
||||||
-> not_null<Media::Player::FloatDelegate*>;
|
-> not_null<Media::Player::FloatDelegate*>;
|
||||||
not_null<Ui::RpWidget*> floatPlayerWidget() override;
|
not_null<Ui::RpWidget*> floatPlayerWidget() override;
|
||||||
|
|
|
@ -31,7 +31,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "main/main_account.h" // Account::sessionValue.
|
#include "main/main_account.h" // Account::sessionValue.
|
||||||
#include "main/main_domain.h"
|
#include "main/main_domain.h"
|
||||||
#include "mainwidget.h"
|
#include "mainwidget.h"
|
||||||
#include "media/system_media_controls_manager.h"
|
|
||||||
#include "ui/boxes/confirm_box.h"
|
#include "ui/boxes/confirm_box.h"
|
||||||
#include "boxes/connection_box.h"
|
#include "boxes/connection_box.h"
|
||||||
#include "storage/storage_account.h"
|
#include "storage/storage_account.h"
|
||||||
|
@ -126,11 +125,6 @@ void MainWindow::initHook() {
|
||||||
this,
|
this,
|
||||||
[=] { checkActivation(); },
|
[=] { checkActivation(); },
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
if (Media::SystemMediaControlsManager::Supported()) {
|
|
||||||
using MediaManager = Media::SystemMediaControlsManager;
|
|
||||||
_mediaControlsManager = std::make_unique<MediaManager>(&controller());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::applyInitialWorkMode() {
|
void MainWindow::applyInitialWorkMode() {
|
||||||
|
|
|
@ -19,10 +19,6 @@ class Widget;
|
||||||
enum class EnterPoint : uchar;
|
enum class EnterPoint : uchar;
|
||||||
} // namespace Intro
|
} // namespace Intro
|
||||||
|
|
||||||
namespace Media {
|
|
||||||
class SystemMediaControlsManager;
|
|
||||||
} // namespace Media
|
|
||||||
|
|
||||||
namespace Window {
|
namespace Window {
|
||||||
class MediaPreviewWidget;
|
class MediaPreviewWidget;
|
||||||
class SectionMemento;
|
class SectionMemento;
|
||||||
|
@ -136,8 +132,6 @@ private:
|
||||||
|
|
||||||
void themeUpdated(const Window::Theme::BackgroundUpdate &data);
|
void themeUpdated(const Window::Theme::BackgroundUpdate &data);
|
||||||
|
|
||||||
std::unique_ptr<Media::SystemMediaControlsManager> _mediaControlsManager;
|
|
||||||
|
|
||||||
QPoint _lastMousePosition;
|
QPoint _lastMousePosition;
|
||||||
|
|
||||||
object_ptr<Window::PasscodeLockWidget> _passcodeLock = { nullptr };
|
object_ptr<Window::PasscodeLockWidget> _passcodeLock = { nullptr };
|
||||||
|
|
|
@ -1280,6 +1280,15 @@ bool Instance::pauseGifByRoundVideo() const {
|
||||||
return _roundPlaying;
|
return _roundPlaying;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Instance::stopAndClose() {
|
||||||
|
_closePlayerRequests.fire({});
|
||||||
|
|
||||||
|
stop(AudioMsgId::Type::Voice);
|
||||||
|
stop(AudioMsgId::Type::Song);
|
||||||
|
|
||||||
|
Shortcuts::ToggleMediaShortcuts(false);
|
||||||
|
}
|
||||||
|
|
||||||
void Instance::handleStreamingUpdate(
|
void Instance::handleStreamingUpdate(
|
||||||
not_null<Data*> data,
|
not_null<Data*> data,
|
||||||
Streaming::Update &&update) {
|
Streaming::Update &&update) {
|
||||||
|
|
|
@ -168,6 +168,11 @@ public:
|
||||||
|
|
||||||
[[nodiscard]] bool pauseGifByRoundVideo() const;
|
[[nodiscard]] bool pauseGifByRoundVideo() const;
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::producer<> closePlayerRequests() const {
|
||||||
|
return _closePlayerRequests.events();
|
||||||
|
}
|
||||||
|
void stopAndClose();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
using SharedMediaType = Storage::SharedMediaType;
|
using SharedMediaType = Storage::SharedMediaType;
|
||||||
using SliceKey = SparseIdsMergedSlice::Key;
|
using SliceKey = SparseIdsMergedSlice::Key;
|
||||||
|
@ -312,6 +317,7 @@ private:
|
||||||
rpl::event_stream<AudioMsgId::Type> _playerStartedPlay;
|
rpl::event_stream<AudioMsgId::Type> _playerStartedPlay;
|
||||||
rpl::event_stream<TrackState> _updatedNotifier;
|
rpl::event_stream<TrackState> _updatedNotifier;
|
||||||
rpl::event_stream<SeekingChanges> _seekingChanges;
|
rpl::event_stream<SeekingChanges> _seekingChanges;
|
||||||
|
rpl::event_stream<> _closePlayerRequests;
|
||||||
rpl::lifetime _lifetime;
|
rpl::lifetime _lifetime;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,14 +14,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "data/data_document.h"
|
#include "data/data_document.h"
|
||||||
#include "data/data_document_media.h"
|
#include "data/data_document_media.h"
|
||||||
#include "data/data_file_origin.h"
|
#include "data/data_file_origin.h"
|
||||||
#include "mainwidget.h"
|
|
||||||
#include "main/main_account.h"
|
#include "main/main_account.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "media/audio/media_audio.h"
|
#include "media/audio/media_audio.h"
|
||||||
#include "media/streaming/media_streaming_instance.h"
|
#include "media/streaming/media_streaming_instance.h"
|
||||||
#include "media/streaming/media_streaming_player.h"
|
#include "media/streaming/media_streaming_player.h"
|
||||||
#include "ui/text/format_song_document_name.h"
|
#include "ui/text/format_song_document_name.h"
|
||||||
#include "window/window_controller.h"
|
|
||||||
|
|
||||||
#include <ksandbox.h>
|
#include <ksandbox.h>
|
||||||
|
|
||||||
|
@ -45,8 +43,7 @@ bool SystemMediaControlsManager::Supported() {
|
||||||
return base::Platform::SystemMediaControls::Supported();
|
return base::Platform::SystemMediaControls::Supported();
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemMediaControlsManager::SystemMediaControlsManager(
|
SystemMediaControlsManager::SystemMediaControlsManager()
|
||||||
not_null<Window::Controller*> controller)
|
|
||||||
: _controls(std::make_unique<base::Platform::SystemMediaControls>()) {
|
: _controls(std::make_unique<base::Platform::SystemMediaControls>()) {
|
||||||
|
|
||||||
using PlaybackStatus =
|
using PlaybackStatus =
|
||||||
|
@ -58,7 +55,7 @@ SystemMediaControlsManager::SystemMediaControlsManager(
|
||||||
_controls->setServiceName(u"tdesktop"_q);
|
_controls->setServiceName(u"tdesktop"_q);
|
||||||
}
|
}
|
||||||
_controls->setApplicationName(AppName.utf16());
|
_controls->setApplicationName(AppName.utf16());
|
||||||
const auto inited = _controls->init(controller->widget());
|
const auto inited = _controls->init();
|
||||||
if (!inited) {
|
if (!inited) {
|
||||||
LOG(("SystemMediaControlsManager failed to init."));
|
LOG(("SystemMediaControlsManager failed to init."));
|
||||||
return;
|
return;
|
||||||
|
@ -227,7 +224,7 @@ SystemMediaControlsManager::SystemMediaControlsManager(
|
||||||
case Command::Next: mediaPlayer->next(type); break;
|
case Command::Next: mediaPlayer->next(type); break;
|
||||||
case Command::Previous: mediaPlayer->previous(type); break;
|
case Command::Previous: mediaPlayer->previous(type); break;
|
||||||
case Command::Stop: mediaPlayer->stop(type); break;
|
case Command::Stop: mediaPlayer->stop(type); break;
|
||||||
case Command::Raise: controller->widget()->showFromTray(); break;
|
case Command::Raise: Core::App().activate(); break;
|
||||||
case Command::LoopNone: {
|
case Command::LoopNone: {
|
||||||
Core::App().settings().setPlayerRepeatMode(RepeatMode::None);
|
Core::App().settings().setPlayerRepeatMode(RepeatMode::None);
|
||||||
Core::App().saveSettingsDelayed();
|
Core::App().saveSettingsDelayed();
|
||||||
|
@ -252,9 +249,7 @@ SystemMediaControlsManager::SystemMediaControlsManager(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Command::Quit: {
|
case Command::Quit: {
|
||||||
if (const auto main = controller->widget()->sessionContent()) {
|
Media::Player::instance()->stopAndClose();
|
||||||
main->closeBothPlayers();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ namespace Media {
|
||||||
|
|
||||||
class SystemMediaControlsManager {
|
class SystemMediaControlsManager {
|
||||||
public:
|
public:
|
||||||
SystemMediaControlsManager(not_null<Window::Controller*> controller);
|
SystemMediaControlsManager();
|
||||||
~SystemMediaControlsManager();
|
~SystemMediaControlsManager();
|
||||||
|
|
||||||
static bool Supported();
|
static bool Supported();
|
||||||
|
|
|
@ -15,7 +15,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "data/stickers/data_stickers.h" // Stickers::setsRef()
|
#include "data/stickers/data_stickers.h" // Stickers::setsRef()
|
||||||
#include "main/main_domain.h"
|
#include "main/main_domain.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "mainwidget.h" // MainWidget::closeBothPlayers
|
|
||||||
#include "media/audio/media_audio_capture.h"
|
#include "media/audio/media_audio_capture.h"
|
||||||
#include "media/player/media_player_instance.h"
|
#include "media/player/media_player_instance.h"
|
||||||
#include "platform/mac/touchbar/mac_touchbar_audio.h"
|
#include "platform/mac/touchbar/mac_touchbar_audio.h"
|
||||||
|
@ -171,9 +170,7 @@ const auto kAudioItemIdentifier = @"touchbarAudio";
|
||||||
autorelease];
|
autorelease];
|
||||||
item.groupTouchBar = touchBar;
|
item.groupTouchBar = touchBar;
|
||||||
[touchBar closeRequests] | rpl::start_with_next([=] {
|
[touchBar closeRequests] | rpl::start_with_next([=] {
|
||||||
if (const auto session = _controller->sessionController()) {
|
Media::Player::instance()->stopAndClose();
|
||||||
session->content()->closeBothPlayers();
|
|
||||||
}
|
|
||||||
}, [item lifetime]);
|
}, [item lifetime]);
|
||||||
return [item autorelease];
|
return [item autorelease];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit a21505416a8e64368925e02f13de2d97f7b476b3
|
Subproject commit 17cac57d9ed5bf8250861a4d11ddf2b4e4e5d641
|
Loading…
Add table
Reference in a new issue