mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-03-21 17:34:57 -05:00
Add option to set content classification of Twitch stream
This commit is contained in:
parent
4e561320f7
commit
4d22a539f0
|
|
@ -1242,6 +1242,14 @@ AdvSceneSwitcher.action.twitch.tags.duplicate="Duplicate Tag"
|
|||
AdvSceneSwitcher.action.twitch.tags.duplicate.info="This tag is already in the list."
|
||||
AdvSceneSwitcher.action.twitch.tags.limit="Tag Limit Reached"
|
||||
AdvSceneSwitcher.action.twitch.tags.limit.info="You can only have up to %1 tags."
|
||||
AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set="Set content classification labels"
|
||||
AdvSceneSwitcher.action.twitch.contentClassification.debatedSocialIssuesAndPolitics="Discussions or debates about politics or sensitive social issues such as elections, civic integrity, military conflict, and civil rights."
|
||||
AdvSceneSwitcher.action.twitch.contentClassification.drugsIntoxication="Excessive tobacco glorification or promotion, any marijuana consumption/use, legal drug and alcohol induced intoxication, discussions of illegal drugs."
|
||||
AdvSceneSwitcher.action.twitch.contentClassification.sexualThemes="Content that focuses on sexualized activities, sexual topics, or experiences."
|
||||
AdvSceneSwitcher.action.twitch.contentClassification.violentGraphic="Simulations and/or depictions of realistic violence, gore, extreme injury, or death."
|
||||
AdvSceneSwitcher.action.twitch.contentClassification.gambling="Participating in or promoting online or in-person gambling, poker, or fantasy sports that involve the exchange of real money."
|
||||
AdvSceneSwitcher.action.twitch.contentClassification.profanityVulgarity="Prolonged and repeated use of obscenities, profanities, and vulgarities, especially as a regular part of speech."
|
||||
AdvSceneSwitcher.action.twitch.contentClassification.getCurrent="Get current content classification labels"
|
||||
AdvSceneSwitcher.action.clipboard="Clipboard"
|
||||
AdvSceneSwitcher.action.clipboard.type.copy.text="Copy text"
|
||||
AdvSceneSwitcher.action.clipboard.type.copy.image="Copy image"
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@ target_sources(
|
|||
chat-connection.hpp
|
||||
chat-message-pattern.cpp
|
||||
chat-message-pattern.hpp
|
||||
content-classification.cpp
|
||||
content-classification.hpp
|
||||
event-sub.cpp
|
||||
event-sub.hpp
|
||||
macro-action-twitch.cpp
|
||||
|
|
|
|||
206
plugins/twitch/content-classification.cpp
Normal file
206
plugins/twitch/content-classification.cpp
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
#include "content-classification.hpp"
|
||||
#include "channel-selection.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "twitch-helpers.hpp"
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
void ContentClassification::Load(obs_data_t *obj)
|
||||
{
|
||||
OBSDataAutoRelease data =
|
||||
obs_data_get_obj(obj, "contentClassificationLabels");
|
||||
_debatedSocialIssuesAndPolitics =
|
||||
obs_data_get_bool(data, "debatedSocialIssuesAndPolitics");
|
||||
_drugsIntoxication = obs_data_get_bool(data, "drugsIntoxication");
|
||||
_sexualThemes = obs_data_get_bool(data, "sexualThemes");
|
||||
_violentGraphic = obs_data_get_bool(data, "violentGraphic");
|
||||
_gambling = obs_data_get_bool(data, "gambling");
|
||||
_profanityVulgarity = obs_data_get_bool(data, "profanityVulgarity");
|
||||
}
|
||||
|
||||
void ContentClassification::Save(obs_data_t *obj) const
|
||||
{
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_bool(data, "debatedSocialIssuesAndPolitics",
|
||||
_debatedSocialIssuesAndPolitics);
|
||||
obs_data_set_bool(data, "drugsIntoxication", _drugsIntoxication);
|
||||
obs_data_set_bool(data, "sexualThemes", _sexualThemes);
|
||||
obs_data_set_bool(data, "violentGraphic", _violentGraphic);
|
||||
obs_data_set_bool(data, "gambling", _gambling);
|
||||
obs_data_set_bool(data, "profanityVulgarity", _profanityVulgarity);
|
||||
obs_data_set_obj(obj, "contentClassificationLabels", data);
|
||||
}
|
||||
|
||||
void ContentClassification::SetContentClassification(
|
||||
const TwitchToken &token) const
|
||||
{
|
||||
OBSDataArrayAutoRelease ccls = obs_data_array_create();
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_string(data, "id", "DebatedSocialIssuesAndPolitics");
|
||||
obs_data_set_bool(data, "is_enabled", _debatedSocialIssuesAndPolitics);
|
||||
obs_data_array_push_back(ccls, data);
|
||||
data = obs_data_create();
|
||||
obs_data_set_string(data, "id", "DrugsIntoxication");
|
||||
obs_data_set_bool(data, "is_enabled", _drugsIntoxication);
|
||||
obs_data_array_push_back(ccls, data);
|
||||
data = obs_data_create();
|
||||
obs_data_set_string(data, "id", "SexualThemes");
|
||||
obs_data_set_bool(data, "is_enabled", _sexualThemes);
|
||||
obs_data_array_push_back(ccls, data);
|
||||
data = obs_data_create();
|
||||
obs_data_set_string(data, "id", "ViolentGraphic");
|
||||
obs_data_set_bool(data, "is_enabled", _violentGraphic);
|
||||
obs_data_array_push_back(ccls, data);
|
||||
data = obs_data_create();
|
||||
obs_data_set_string(data, "id", "Gambling");
|
||||
obs_data_set_bool(data, "is_enabled", _gambling);
|
||||
obs_data_array_push_back(ccls, data);
|
||||
data = obs_data_create();
|
||||
obs_data_set_string(data, "id", "ProfanityVulgarity");
|
||||
obs_data_set_bool(data, "is_enabled", _profanityVulgarity);
|
||||
obs_data_array_push_back(ccls, data);
|
||||
|
||||
data = obs_data_create();
|
||||
obs_data_set_array(data, "content_classification_labels", ccls);
|
||||
|
||||
auto result = SendPatchRequest(token, "https://api.twitch.tv",
|
||||
"/helix/channels",
|
||||
{{"broadcaster_id", token.GetUserID()}},
|
||||
data.Get());
|
||||
|
||||
if (result.status != 204) {
|
||||
blog(LOG_INFO,
|
||||
"Failed to set stream content classification labels! (%d)",
|
||||
result.status);
|
||||
}
|
||||
}
|
||||
|
||||
ContentClassificationEdit::ContentClassificationEdit(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
_debatedSocialIssuesAndPolitics(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.contentClassification.debatedSocialIssuesAndPolitics"),
|
||||
this)),
|
||||
_drugsIntoxication(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.contentClassification.drugsIntoxication"),
|
||||
this)),
|
||||
_sexualThemes(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.contentClassification.sexualThemes"),
|
||||
this)),
|
||||
_violentGraphic(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.contentClassification.violentGraphic"),
|
||||
this)),
|
||||
_gambling(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.contentClassification.gambling"),
|
||||
this)),
|
||||
_profanityVulgarity(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.contentClassification.profanityVulgarity"),
|
||||
this))
|
||||
{
|
||||
auto getCurrent = new QPushButton(obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.contentClassification.getCurrent"));
|
||||
connect(getCurrent, &QPushButton::clicked, this,
|
||||
&ContentClassificationEdit::GetCurrentClicked);
|
||||
|
||||
const auto emitChangeSignal = [this]() {
|
||||
ContentClassification ccl;
|
||||
ccl._debatedSocialIssuesAndPolitics =
|
||||
_debatedSocialIssuesAndPolitics->isChecked();
|
||||
ccl._drugsIntoxication = _drugsIntoxication->isChecked();
|
||||
ccl._sexualThemes = _sexualThemes->isChecked();
|
||||
ccl._violentGraphic = _violentGraphic->isChecked();
|
||||
ccl._gambling = _gambling->isChecked();
|
||||
ccl._profanityVulgarity = _profanityVulgarity->isChecked();
|
||||
emit ContentClassificationChanged(ccl);
|
||||
};
|
||||
|
||||
connect(_debatedSocialIssuesAndPolitics, &QCheckBox::stateChanged, this,
|
||||
emitChangeSignal);
|
||||
connect(_drugsIntoxication, &QCheckBox::stateChanged, this,
|
||||
emitChangeSignal);
|
||||
connect(_sexualThemes, &QCheckBox::stateChanged, this,
|
||||
emitChangeSignal);
|
||||
connect(_violentGraphic, &QCheckBox::stateChanged, this,
|
||||
emitChangeSignal);
|
||||
connect(_gambling, &QCheckBox::stateChanged, this, emitChangeSignal);
|
||||
connect(_profanityVulgarity, &QCheckBox::stateChanged, this,
|
||||
emitChangeSignal);
|
||||
|
||||
auto layout = new QVBoxLayout;
|
||||
layout->addWidget(_debatedSocialIssuesAndPolitics);
|
||||
layout->addWidget(_drugsIntoxication);
|
||||
layout->addWidget(_sexualThemes);
|
||||
layout->addWidget(_violentGraphic);
|
||||
layout->addWidget(_gambling);
|
||||
layout->addWidget(_profanityVulgarity);
|
||||
layout->addWidget(getCurrent);
|
||||
setLayout(layout);
|
||||
|
||||
// TODO:
|
||||
// Figure out why the Twitch API always returns an empty CC list
|
||||
// Hide for now
|
||||
getCurrent->hide();
|
||||
}
|
||||
|
||||
void ContentClassificationEdit::SetContentClassification(
|
||||
const ContentClassification &ccl)
|
||||
{
|
||||
_debatedSocialIssuesAndPolitics->setChecked(
|
||||
ccl._debatedSocialIssuesAndPolitics);
|
||||
_drugsIntoxication->setChecked(ccl._drugsIntoxication);
|
||||
_sexualThemes->setChecked(ccl._sexualThemes);
|
||||
_violentGraphic->setChecked(ccl._violentGraphic);
|
||||
_gambling->setChecked(ccl._gambling);
|
||||
_profanityVulgarity->setChecked(ccl._profanityVulgarity);
|
||||
}
|
||||
|
||||
void ContentClassificationEdit::SetToken(const std::weak_ptr<TwitchToken> &t)
|
||||
{
|
||||
_token = t;
|
||||
}
|
||||
|
||||
void ContentClassificationEdit::GetCurrentClicked()
|
||||
{
|
||||
auto token = _token.lock();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
TwitchChannel channel;
|
||||
channel.SetName(token->Name());
|
||||
const auto channelInfo = channel.GetInfo(*token);
|
||||
if (!channelInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContentClassification ccl;
|
||||
for (const auto &label : channelInfo->content_classification_labels) {
|
||||
if (label == "DebatedSocialIssuesAndPolitics") {
|
||||
ccl._debatedSocialIssuesAndPolitics = true;
|
||||
} else if (label == "DrugsIntoxication") {
|
||||
ccl._drugsIntoxication = true;
|
||||
} else if (label == "SexualThemes") {
|
||||
ccl._sexualThemes = true;
|
||||
} else if (label == "ViolentGraphic") {
|
||||
ccl._violentGraphic = true;
|
||||
} else if (label == "Gambling") {
|
||||
ccl._gambling = true;
|
||||
} else if (label == "ProfanityVulgarity") {
|
||||
ccl._profanityVulgarity = true;
|
||||
}
|
||||
}
|
||||
|
||||
SetContentClassification(ccl);
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
50
plugins/twitch/content-classification.hpp
Normal file
50
plugins/twitch/content-classification.hpp
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#pragma once
|
||||
#include "token.hpp"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QCheckBox;
|
||||
|
||||
namespace advss {
|
||||
|
||||
struct ContentClassification {
|
||||
void Load(obs_data_t *obj);
|
||||
void Save(obs_data_t *obj) const;
|
||||
void SetContentClassification(const TwitchToken &token) const;
|
||||
|
||||
private:
|
||||
bool _debatedSocialIssuesAndPolitics = false;
|
||||
bool _drugsIntoxication = false;
|
||||
bool _sexualThemes = false;
|
||||
bool _violentGraphic = false;
|
||||
bool _gambling = false;
|
||||
bool _profanityVulgarity = false;
|
||||
|
||||
friend class ContentClassificationEdit;
|
||||
};
|
||||
|
||||
class ContentClassificationEdit final : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ContentClassificationEdit(QWidget *parent = nullptr);
|
||||
void SetContentClassification(const ContentClassification &ccl);
|
||||
void SetToken(const std::weak_ptr<TwitchToken> &token);
|
||||
|
||||
private slots:
|
||||
void GetCurrentClicked();
|
||||
signals:
|
||||
void ContentClassificationChanged(const ContentClassification &);
|
||||
|
||||
private:
|
||||
QCheckBox *_debatedSocialIssuesAndPolitics;
|
||||
QCheckBox *_drugsIntoxication;
|
||||
QCheckBox *_sexualThemes;
|
||||
QCheckBox *_violentGraphic;
|
||||
QCheckBox *_gambling;
|
||||
QCheckBox *_profanityVulgarity;
|
||||
|
||||
std::weak_ptr<TwitchToken> _token;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
|
|
@ -59,6 +59,8 @@ const static std::map<MacroActionTwitch::Action, std::string> actionTypes = {
|
|||
"AdvSceneSwitcher.action.twitch.type.channel.info.category.set"},
|
||||
{MacroActionTwitch::Action::CHANNEL_INFO_TAGS_SET,
|
||||
"AdvSceneSwitcher.action.twitch.type.channel.info.tags.set"},
|
||||
{MacroActionTwitch::Action::CHANNEL_INFO_CONTENT_LABELS_SET,
|
||||
"AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set"},
|
||||
{MacroActionTwitch::Action::RAID_START,
|
||||
"AdvSceneSwitcher.action.twitch.type.raid.start"},
|
||||
{MacroActionTwitch::Action::COMMERCIAL_START,
|
||||
|
|
@ -523,6 +525,9 @@ bool MacroActionTwitch::PerformAction()
|
|||
case Action::CHANNEL_INFO_TAGS_SET:
|
||||
_tags.SetStreamTags(*token);
|
||||
break;
|
||||
case Action::CHANNEL_INFO_CONTENT_LABELS_SET:
|
||||
_contentClassification.SetContentClassification(*token);
|
||||
break;
|
||||
case Action::RAID_START:
|
||||
StartRaid(token);
|
||||
break;
|
||||
|
|
@ -583,6 +588,7 @@ bool MacroActionTwitch::Save(obs_data_t *obj) const
|
|||
_streamTitle.Save(obj, "streamTitle");
|
||||
_category.Save(obj);
|
||||
_tags.Save(obj);
|
||||
_contentClassification.Save(obj);
|
||||
_markerDescription.Save(obj, "markerDescription");
|
||||
obs_data_set_bool(obj, "clipHasDelay", _clipHasDelay);
|
||||
_duration.Save(obj);
|
||||
|
|
@ -612,6 +618,7 @@ bool MacroActionTwitch::Load(obs_data_t *obj)
|
|||
_streamTitle.Load(obj, "streamTitle");
|
||||
_category.Load(obj);
|
||||
_tags.Load(obj);
|
||||
_contentClassification.Load(obj);
|
||||
_markerDescription.Load(obj, "markerDescription");
|
||||
_clipHasDelay = obs_data_get_bool(obj, "clipHasDelay");
|
||||
_duration.Load(obj);
|
||||
|
|
@ -652,6 +659,8 @@ bool MacroActionTwitch::ActionIsSupportedByToken()
|
|||
{{"channel:manage:broadcast"}}},
|
||||
{Action::CHANNEL_INFO_TAGS_SET,
|
||||
{{"channel:manage:broadcast"}}},
|
||||
{Action::CHANNEL_INFO_CONTENT_LABELS_SET,
|
||||
{{"channel:manage:broadcast"}}},
|
||||
{Action::CHANNEL_INFO_LANGUAGE_SET,
|
||||
{{"channel:manage:broadcast"}}},
|
||||
{Action::CHANNEL_INFO_DELAY_SET,
|
||||
|
|
@ -826,6 +835,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
|
|||
_streamTitle(new VariableLineEdit(this)),
|
||||
_category(new TwitchCategoryWidget(this)),
|
||||
_tags(new TagListWidget(this)),
|
||||
_contentClassification(new ContentClassificationEdit(this)),
|
||||
_markerDescription(new VariableLineEdit(this)),
|
||||
_clipHasDelay(new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.clip.hasDelay"))),
|
||||
|
|
@ -852,6 +862,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
|
|||
mainLayout->addWidget(_announcementMessage);
|
||||
mainLayout->addWidget(_chatMessage);
|
||||
mainLayout->addWidget(_tags);
|
||||
mainLayout->addWidget(_contentClassification);
|
||||
mainLayout->addWidget(_tokenWarning);
|
||||
setLayout(mainLayout);
|
||||
|
||||
|
|
@ -889,6 +900,7 @@ void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token)
|
|||
_channel->SetToken(_entryData->_token);
|
||||
_pointsReward->SetToken(_entryData->_token);
|
||||
_tags->SetToken(_entryData->_token);
|
||||
_contentClassification->SetToken(_entryData->_token);
|
||||
_entryData->ResetChatConnection();
|
||||
|
||||
SetWidgetVisibility();
|
||||
|
|
@ -949,6 +961,13 @@ void MacroActionTwitchEdit::TagsChanged(const TwitchTagList &tags)
|
|||
_entryData->_tags = tags;
|
||||
}
|
||||
|
||||
void MacroActionTwitchEdit::ContentClassificationChanged(
|
||||
const ContentClassification &ccl)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_contentClassification = ccl;
|
||||
}
|
||||
|
||||
void MacroActionTwitchEdit::MarkerDescriptionChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
|
|
@ -1031,6 +1050,12 @@ void MacroActionTwitchEdit::SetWidgetSignalConnections()
|
|||
SLOT(CategoryChanged(const TwitchCategory &)));
|
||||
QWidget::connect(_tags, SIGNAL(TagListChanged(const TwitchTagList &)),
|
||||
this, SLOT(TagsChanged(const TwitchTagList &)));
|
||||
QWidget::connect(_contentClassification,
|
||||
SIGNAL(ContentClassificationChanged(
|
||||
const ContentClassification &)),
|
||||
this,
|
||||
SLOT(ContentClassificationChanged(
|
||||
const ContentClassification &)));
|
||||
QWidget::connect(_markerDescription, SIGNAL(editingFinished()), this,
|
||||
SLOT(MarkerDescriptionChanged()));
|
||||
QObject::connect(_clipHasDelay, SIGNAL(stateChanged(int)), this,
|
||||
|
|
@ -1075,6 +1100,9 @@ void MacroActionTwitchEdit::SetWidgetVisibility()
|
|||
MacroActionTwitch::Action::CHANNEL_INFO_CATEGORY_SET);
|
||||
_tags->setVisible(_entryData->GetAction() ==
|
||||
MacroActionTwitch::Action::CHANNEL_INFO_TAGS_SET);
|
||||
_contentClassification->setVisible(
|
||||
_entryData->GetAction() ==
|
||||
MacroActionTwitch::Action::CHANNEL_INFO_CONTENT_LABELS_SET);
|
||||
_channel->setVisible(
|
||||
_entryData->GetAction() ==
|
||||
MacroActionTwitch::Action::RAID_START ||
|
||||
|
|
@ -1272,6 +1300,9 @@ void MacroActionTwitchEdit::UpdateEntryData()
|
|||
_category->SetCategory(_entryData->_category);
|
||||
_tags->SetTags(_entryData->_tags);
|
||||
_tags->SetToken(_entryData->_token);
|
||||
_contentClassification->SetContentClassification(
|
||||
_entryData->_contentClassification);
|
||||
_contentClassification->SetToken(_entryData->_token);
|
||||
_markerDescription->setText(_entryData->_markerDescription);
|
||||
_clipHasDelay->setChecked(_entryData->_clipHasDelay);
|
||||
_duration->SetDuration(_entryData->_duration);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "category-selection.hpp"
|
||||
#include "channel-selection.hpp"
|
||||
#include "chat-connection.hpp"
|
||||
#include "content-classification.hpp"
|
||||
#include "points-reward-selection.hpp"
|
||||
#include "tag-selection.hpp"
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ public:
|
|||
CHANNEL_INFO_LANGUAGE_SET = 30,
|
||||
CHANNEL_INFO_DELAY_SET = 40,
|
||||
CHANNEL_INFO_TAGS_SET = 50,
|
||||
CHANNEL_INFO_CONTENT_LABELS_SET = 60, // TODO
|
||||
CHANNEL_INFO_CONTENT_LABELS_SET = 60,
|
||||
CHANNEL_INFO_BRANDED_CONTENT_ENABLE = 70,
|
||||
CHANNEL_INFO_BRANDED_CONTENT_DISABLE = 71,
|
||||
|
||||
|
|
@ -170,6 +171,7 @@ public:
|
|||
obs_module_text("AdvSceneSwitcher.action.twitch.title.title");
|
||||
TwitchCategory _category;
|
||||
TwitchTagList _tags;
|
||||
ContentClassification _contentClassification;
|
||||
StringVariable _markerDescription = obs_module_text(
|
||||
"AdvSceneSwitcher.action.twitch.marker.description");
|
||||
bool _clipHasDelay = false;
|
||||
|
|
@ -238,6 +240,7 @@ private slots:
|
|||
void StreamTitleChanged();
|
||||
void CategoryChanged(const TwitchCategory &);
|
||||
void TagsChanged(const TwitchTagList &tags);
|
||||
void ContentClassificationChanged(const ContentClassification &ccl);
|
||||
void MarkerDescriptionChanged();
|
||||
void ClipHasDelayChanged(int state);
|
||||
void DurationChanged(const Duration &);
|
||||
|
|
@ -273,6 +276,7 @@ private:
|
|||
VariableLineEdit *_streamTitle;
|
||||
TwitchCategoryWidget *_category;
|
||||
TagListWidget *_tags;
|
||||
ContentClassificationEdit *_contentClassification;
|
||||
VariableLineEdit *_markerDescription;
|
||||
QCheckBox *_clipHasDelay;
|
||||
DurationSelection *_duration;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user