Compare commits

..

23 Commits

Author SHA1 Message Date
WarmUpTill
70bbc7cdac Implement proper timestamp validation for Twitch messages
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2025-04-11 19:02:37 +02:00
WarmUpTill
d892298995 Add missing "[adv-ss]" log tag 2025-04-11 19:02:37 +02:00
WarmUpTill
0fe31432be Add "previous scene" to the "scene has (not) changed" checks 2025-04-11 18:57:58 +02:00
WarmUpTill
aaa0113ccb Ignore Xerrors 2025-04-11 18:57:25 +02:00
WarmUpTill
b908954b46 CI: Add cmake setup step to Linux build
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2025-04-02 13:48:23 +02:00
WarmUpTill
b0eede8a85 Add "disable" effect to macro conditions using "ignore" logic selection 2025-04-02 13:48:23 +02:00
WarmUpTill
aa87911b71 Use cpp-httplib based HTTP action type
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
The goal is to remove the older, more limited version of the HTTP action
at some point in the future.
2025-03-30 14:06:19 +02:00
WarmUpTill
1b05019acc Add option to hide entries from action selection
Intended to be used to hide legacy version of actions types (e.g. HTTP)
2025-03-30 14:01:41 +02:00
WarmUpTill
78a5a2629d Hide the "remote" file check option
This option will be removed at some point in the future.
The http action should be used instead.
2025-03-30 14:01:41 +02:00
WarmUpTill
634270a978 Cleanup includes 2025-03-30 14:01:41 +02:00
WarmUpTill
78ba22e1e4 Hide "get settings" button when setting macro property value 2025-03-30 14:01:41 +02:00
WarmUpTill
aba5737a60 Update and clean up locale (qwe1154323937) 2025-03-30 14:01:41 +02:00
WarmUpTill
4315f7f621 Exclude unwanted files from sources archive 2025-03-30 14:01:41 +02:00
WarmUpTill
53c535962f Use tab key to switch to dialog controls and set default focus to input 2025-03-30 14:01:41 +02:00
WarmUpTill
1e718b78c7 Fall back to project version if git tag cannot be queried 2025-03-30 14:01:41 +02:00
WarmUpTill
b1d2156228 Update libremidi to v4.3.0 2025-03-30 14:01:41 +02:00
WarmUpTill
9c3c953c6b Ignore deprecation warnings for Qt 6.7 and above
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2025-03-22 18:09:51 +01:00
WarmUpTill
ae74f68db7 Add option to close projector windows 2025-03-22 18:09:51 +01:00
WarmUpTill
213f1bba36 Add "Any" entry of the reward selection only for Twitch condition 2025-03-22 18:09:51 +01:00
WarmUpTill
5a2cb0bd68 Add more scripting signals / procedures 2025-03-22 18:09:51 +01:00
WarmUpTill
23b461828b Add start start / stop callbacks 2025-03-22 18:09:51 +01:00
WarmUpTill
9944a1b03b Move interval reset handling 2025-03-22 18:09:51 +01:00
WarmUpTill
eaad4d1bbd Fix Twitch helper caches misbehaving
API calls with different sets of arguments could map to the same key
value pair, which resulted in unexpected behavior for functions using
those caches
2025-03-22 18:09:51 +01:00
62 changed files with 3391 additions and 657 deletions

View File

@@ -210,6 +210,11 @@ jobs:
restore-keys: |
${{ runner.os }}-ccache-x86_64-
- name: Set up CMake 🏗️
uses: jwlawson/actions-setup-cmake@v1.13
with:
cmake-version: '3.24.x'
- name: Set up Homebrew 🍺
uses: Homebrew/actions/setup-homebrew@master

3
.gitmodules vendored
View File

@@ -28,3 +28,6 @@
[submodule "deps/libusb"]
path = deps/libusb
url = https://github.com/libusb/libusb.git
[submodule "deps/date"]
path = deps/date
url = https://github.com/HowardHinnant/date.git

View File

@@ -33,6 +33,11 @@ add_library(${LIB_NAME} SHARED)
include(cmake/common/get_git_revision_description.cmake)
get_git_head_revision(GIT_REFSPEC GIT_SHA1)
git_describe(GIT_TAG)
if(${GIT_TAG} STREQUAL "GIT-NOTFOUND")
set(GIT_TAG ${PROJECT_VERSION})
endif()
message(STATUS "${PROJECT_NAME} version: ${GIT_TAG}")
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/version.cpp.in"
"${CMAKE_CURRENT_BINARY_DIR}/lib/version.cpp" @ONLY)
@@ -298,6 +303,13 @@ setup_obs_lib_dependency(${PROJECT_NAME})
find_package(Qt6 REQUIRED COMPONENTS Widgets Core)
target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Core Qt6::Widgets)
target_link_libraries(${LIB_NAME} PRIVATE Qt6::Core Qt6::Widgets)
# Ignore QCheckBox::stateChanged deprecation warning until minimum supported Qt
# version is at least Qt 6.7, which introduces QCheckBox::checkStateChanged
if(Qt6_VERSION VERSION_GREATER "6.0.0")
target_compile_definitions(${LIB_NAME} PRIVATE QT_NO_DEPRECATED_WARNINGS)
endif()
target_compile_options(
${PROJECT_NAME}
PRIVATE
@@ -431,6 +443,9 @@ else()
endif()
target_include_directories(${LIB_NAME} PRIVATE "${PROC_INCLUDE_DIR}")
target_sources(${LIB_NAME} PRIVATE lib/linux/advanced-scene-switcher-nix.cpp)
# Don't include irrelevant folders into sources archive
list(APPEND CPACK_SOURCE_IGNORE_FILES "\\.deps/.*")
endif()
if(NOT OS_WINDOWS)

View File

@@ -286,6 +286,12 @@ function(setup_advss_plugin target)
find_package(Qt6 REQUIRED COMPONENTS Widgets Core)
target_link_libraries(${target} PRIVATE Qt6::Core Qt6::Widgets)
# Ignore QCheckBox::stateChanged deprecation warning until minimum supported
# Qt version is at least Qt 6.7, which introduces QCheckBox::checkStateChanged
if(Qt6_VERSION VERSION_GREATER "6.7.0")
target_compile_definitions(${target} PRIVATE QT_NO_DEPRECATED_WARNINGS)
endif()
set_target_properties(
${target}
PROPERTIES AUTOMOC ON

View File

@@ -526,7 +526,6 @@ AdvSceneSwitcher.action.screenshot.save.default="Standard"
AdvSceneSwitcher.action.screenshot.save.custom="Benutzerdefiniert"
AdvSceneSwitcher.action.screenshot.type.source="Quelle"
AdvSceneSwitcher.action.screenshot.type.scene="Szene"
AdvSceneSwitcher.action.screenshot.mainOutput="OBS's Haupt-Ausgabe"
AdvSceneSwitcher.action.screenshot.blackscreenNote="Quellen oder Szenen, die nicht immer gerendert werden, können dazu führen, dass einige Teile der Screenshots leer bleiben."
AdvSceneSwitcher.action.screenshot.entry="Screenshot{{targetType}}{{sources}}{{scenes}}und speichere in{{saveType}}{{variables}}Pfad"
AdvSceneSwitcher.action.profile="Profil"

View File

@@ -1022,7 +1022,6 @@ AdvSceneSwitcher.action.screenshot.save.custom="Custom file path"
AdvSceneSwitcher.action.screenshot.save.variable="Variable (base64 encoded PNG)"
AdvSceneSwitcher.action.screenshot.type.source="Source"
AdvSceneSwitcher.action.screenshot.type.scene="Scene"
AdvSceneSwitcher.action.screenshot.mainOutput="OBS's main output"
AdvSceneSwitcher.action.screenshot.blackscreenNote="Sources or scenes, which are not always rendered, may result in some parts of screenshots to remain blank."
AdvSceneSwitcher.action.screenshot.entry="Screenshot{{targetType}}{{sources}}{{scenes}}and save to{{saveType}}{{variables}}"
AdvSceneSwitcher.action.profile="Profile"
@@ -1051,11 +1050,25 @@ AdvSceneSwitcher.action.websocket.entry.sceneSwitcher.request="Send{{api}}of typ
AdvSceneSwitcher.action.websocket.entry.sceneSwitcher.event="Send{{api}}of type{{type}}to connected clients"
AdvSceneSwitcher.action.websocket.entry.generic="Send{{api}}via{{connection}}"
AdvSceneSwitcher.action.http="HTTP"
AdvSceneSwitcher.action.http.legacy="HTTP (legacy)"
AdvSceneSwitcher.action.http.setHeaders="Set headers"
AdvSceneSwitcher.action.http.headers="Headers:"
AdvSceneSwitcher.action.http.addHeader="Add header"
AdvSceneSwitcher.action.http.addHeader.name="Header name"
AdvSceneSwitcher.action.http.addHeader.value="Header value"
AdvSceneSwitcher.action.http.setParams="Set parameters"
AdvSceneSwitcher.action.http.params="Parameters:"
AdvSceneSwitcher.action.http.addParam.name="Parameter name"
AdvSceneSwitcher.action.http.addParam.value="Parameter value"
AdvSceneSwitcher.action.http.body="Message body:"
AdvSceneSwitcher.action.http.type.get="GET"
AdvSceneSwitcher.action.http.type.post="POST"
AdvSceneSwitcher.action.http.type.put="PUT"
AdvSceneSwitcher.action.http.type.patch="PATCH"
AdvSceneSwitcher.action.http.type.delete="DELETE"
AdvSceneSwitcher.action.http.layout.method="Send{{method}}to URL{{url}}at path{{path}}"
AdvSceneSwitcher.action.http.layout.contentType="Content type:{{contentType}}"
AdvSceneSwitcher.action.http.layout.timeout="Timeout:{{timeout}}seconds"
AdvSceneSwitcher.action.http.entry.line1="Send{{method}}to{{url}}"
AdvSceneSwitcher.action.http.entry.line2="Timeout:{{timeout}}seconds"
AdvSceneSwitcher.action.variable="Variable"
@@ -1113,6 +1126,8 @@ AdvSceneSwitcher.action.variable.entry.userInput.customPrompt="{{useCustomPrompt
AdvSceneSwitcher.action.variable.entry.userInput.placeholder="{{useInputPlaceholder}}Fill with placeholder{{inputPlaceholder}}"
AdvSceneSwitcher.action.variable.entry.randomNumber="Generate random number in range from{{randomNumberStart}}to{{randomNumberEnd}}"
AdvSceneSwitcher.action.projector="Projector"
AdvSceneSwitcher.action.projector.action.open="Open"
AdvSceneSwitcher.action.projector.action.close="Close"
AdvSceneSwitcher.action.projector.type.source="Source"
AdvSceneSwitcher.action.projector.type.scene="Scene"
AdvSceneSwitcher.action.projector.type.preview="Preview"
@@ -1121,8 +1136,9 @@ AdvSceneSwitcher.action.projector.type.multiview="Multiview"
AdvSceneSwitcher.action.projector.display="Display"
AdvSceneSwitcher.action.projector.windowed="Windowed"
AdvSceneSwitcher.action.projector.fullscreen="Fullscreen"
AdvSceneSwitcher.action.projector.entry="Open{{windowTypes}}projector of{{types}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.projector.entry.monitor="on{{monitors}}"
AdvSceneSwitcher.action.projector.entry.close="{{actions}}projector with name{{projectorWindowName}}{{regex}}"
AdvSceneSwitcher.action.projector.entry.open.windowed="{{actions}}{{windowTypes}}projector of{{types}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.projector.entry.open.fullscreen="{{actions}}{{windowTypes}}projector of{{types}}{{scenes}}{{sources}}on{{monitors}}"
AdvSceneSwitcher.action.midi="MIDI"
AdvSceneSwitcher.action.midi.entry="Send message to{{device}}:"
AdvSceneSwitcher.action.midi.entry.listen="Set MIDI message selection to messages incoming on{{listenDevices}}:{{listenButton}}"
@@ -2038,6 +2054,11 @@ AdvSceneSwitcher.tempVar.gameCapture.class.description="Window class of the appl
AdvSceneSwitcher.tempVar.gameCapture.executable="Executable"
AdvSceneSwitcher.tempVar.gameCapture.executable.description="Executable name of the application captured by the source."
AdvSceneSwitcher.tempVar.http.status="Status code"
AdvSceneSwitcher.tempVar.http.body="Message body"
AdvSceneSwitcher.tempVar.http.error="Error"
AdvSceneSwitcher.tempVar.http.error.description="Empty when no error occurred.\nOther possible values:\n\n * Could not establish connection\n * Failed to bind IP address\n * Failed to read connection\n * Failed to write connection\n * Maximum redirect count exceeded\n * Connection handling canceled\n * SSL connection failed\n * SSL certificate loading failed\n * SSL server verification failed\n * Unsupported HTTP multipart boundary characters\n * Compression failed\n * Connection timed out\n * Proxy connection failed\n * Unknown"
AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
AdvSceneSwitcher.selectCurrentScene="Current Scene"
@@ -2076,7 +2097,6 @@ AdvSceneSwitcher.settings.suffix.type.list=" (List)"
AdvSceneSwitcher.settings.suffix.type.color=" (Color)"
AdvSceneSwitcher.settings.suffix.type.button=" (Button)"
AdvSceneSwitcher.settings.suffix.type.font=" (Font)"
AdvSceneSwitcher.settings.suffix.type.font=" (Font)"
AdvSceneSwitcher.settings.suffix.type.editableList=" (Editable list)"
AdvSceneSwitcher.settings.suffix.type.frameRate=" (Frame rate)"
AdvSceneSwitcher.settings.suffix.type.group=" (Group)"

View File

@@ -438,7 +438,6 @@ AdvSceneSwitcher.action.random="Aleatorio"
AdvSceneSwitcher.action.random.entry="Ejecute aleatoriamente cualquiera de las siguientes macros (las macros en pausa se ignoran)"
AdvSceneSwitcher.action.systray="Notificación de la bandeja del sistema"
AdvSceneSwitcher.action.screenshot="Captura de pantalla"
AdvSceneSwitcher.action.screenshot.mainOutput="Salida principal de OBS"
AdvSceneSwitcher.action.profile="Perfil"
AdvSceneSwitcher.action.profile.entry="Cambiar perfil activo a {{profiles}}"
AdvSceneSwitcher.action.sceneCollection="Colección de escenas"

View File

@@ -647,7 +647,6 @@ AdvSceneSwitcher.action.screenshot.save.default="Par défaut"
AdvSceneSwitcher.action.screenshot.save.custom="Personnalisé"
AdvSceneSwitcher.action.screenshot.type.source="Source"
AdvSceneSwitcher.action.screenshot.type.scene="Scène"
AdvSceneSwitcher.action.screenshot.mainOutput="Sortie principale d'OBS"
AdvSceneSwitcher.action.screenshot.blackscreenNote="Les sources ou les scènes qui ne sont pas toujours rendues peuvent entraîner des parties de captures d'écran vides."
AdvSceneSwitcher.action.screenshot.entry="Capturer{{targetType}}{{sources}}{{scenes}}et enregistrer à l'emplacement{{saveType}}{{variables}}"
AdvSceneSwitcher.action.profile="Profil"
@@ -718,8 +717,6 @@ AdvSceneSwitcher.action.projector.type.multiview="Multivue"
AdvSceneSwitcher.action.projector.display="Affichage"
AdvSceneSwitcher.action.projector.windowed="Fenêtré"
AdvSceneSwitcher.action.projector.fullscreen="Plein écran"
AdvSceneSwitcher.action.projector.entry="Ouvrir le projecteur{{windowTypes}}de{{types}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.projector.entry.monitor="sur{{monitors}}"
AdvSceneSwitcher.action.midi="MIDI"
AdvSceneSwitcher.action.midi.entry="Envoyer un message à{{device}}:"
AdvSceneSwitcher.action.midi.entry.listen="Définir la sélection de messages MIDI sur les messages entrants de{{listenDevices}}:{{listenButton}}"

View File

@@ -979,7 +979,6 @@ AdvSceneSwitcher.action.screenshot.save.default="デフォルト"
AdvSceneSwitcher.action.screenshot.save.custom="カスタム"
; AdvSceneSwitcher.action.screenshot.type.source="Source"
AdvSceneSwitcher.action.screenshot.type.scene="シーン"
AdvSceneSwitcher.action.screenshot.mainOutput="OBSの主な出力"
AdvSceneSwitcher.action.screenshot.blackscreenNote="常にレンダリングされるわけではないソースやシーンにより、スクリーンショットの一部が空白のままになる場合があります。"
AdvSceneSwitcher.action.screenshot.entry="スクリーンショット{{targetType}}{{sources}}{{scenes}}を作成し、{{saveType}}{{variables}}の場所に保存します"
AdvSceneSwitcher.action.profile="プロファイル"
@@ -1068,8 +1067,6 @@ AdvSceneSwitcher.action.projector.type.multiview="マルチビュー"
AdvSceneSwitcher.action.projector.display="ディスプレイ"
AdvSceneSwitcher.action.projector.windowed="ウィンドウ"
AdvSceneSwitcher.action.projector.fullscreen="フルスクリーン"
AdvSceneSwitcher.action.projector.entry="{{types}}{{scenes}}{{sources}}の{{windowTypes}}プロジェクターを開きます"
AdvSceneSwitcher.action.projector.entry.monitor="{{monitors}}上で"
; AdvSceneSwitcher.action.midi="MIDI"
AdvSceneSwitcher.action.midi.entry="メッセージを{{device}}に送信します:"
AdvSceneSwitcher.action.midi.entry.listen="MIDI メッセージの選択を {{listenDevices}} で受信するメッセージに設定します:{{listenButton}}"

View File

@@ -960,7 +960,6 @@ AdvSceneSwitcher.action.screenshot.save.default="Padrão"
AdvSceneSwitcher.action.screenshot.save.custom="Personalizado"
AdvSceneSwitcher.action.screenshot.type.source="Fonte"
AdvSceneSwitcher.action.screenshot.type.scene="Cena"
AdvSceneSwitcher.action.screenshot.mainOutput="Saída principal do OBS"
AdvSceneSwitcher.action.screenshot.blackscreenNote="Fontes ou cenas, que nem sempre são renderizadas, podem resultar em algumas partes das capturas de tela permanecendo em branco."
AdvSceneSwitcher.action.screenshot.entry="Captura de tela{{targetType}}{{sources}}{{scenes}}e salvar em{{saveType}}{{variables}}localização"
AdvSceneSwitcher.action.profile="Perfil"
@@ -1049,8 +1048,6 @@ AdvSceneSwitcher.action.projector.type.multiview="Multivisão"
AdvSceneSwitcher.action.projector.display="Exibição"
AdvSceneSwitcher.action.projector.windowed="Janela"
AdvSceneSwitcher.action.projector.fullscreen="Tela cheia"
AdvSceneSwitcher.action.projector.entry="Abrir{{windowTypes}}projetor de{{types}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.projector.entry.monitor="em{{monitors}}"
AdvSceneSwitcher.action.midi="MIDI"
AdvSceneSwitcher.action.midi.entry="Enviar mensagem para{{device}}:"
AdvSceneSwitcher.action.midi.entry.listen="Definir seleção de mensagem MIDI para mensagens recebidas em{{listenDevices}}:{{listenButton}}"

View File

@@ -356,7 +356,6 @@ AdvSceneSwitcher.action.random="Rastgele"
AdvSceneSwitcher.action.random.entry="Aşağıdaki makrolardan herhangi birini rastgele çalıştırın (duraklatılmış makrolar yoksayılır)"
AdvSceneSwitcher.action.systray="Sistem tepsisi bildirimi"
AdvSceneSwitcher.action.screenshot="Ekran görüntüsü"
AdvSceneSwitcher.action.screenshot.mainOutput="OBS'nin ana çıkışı"
AdvSceneSwitcher.action.profile="Profil"
AdvSceneSwitcher.action.profile.entry="Aktif profili şununla değiştir: {{profiles}}"
AdvSceneSwitcher.action.sceneCollection="Sahne koleksiyonu"

File diff suppressed because it is too large Load Diff

1
deps/date vendored Submodule

Submodule deps/date added at 5bdb7e6f31

2
deps/libremidi vendored

View File

@@ -1,6 +1,5 @@
#include "advanced-scene-switcher.hpp"
#include "backup.hpp"
#include "curl-helper.hpp"
#include "log-helper.hpp"
#include "macro-helpers.hpp"
#include "obs-module-helper.hpp"
@@ -19,10 +18,15 @@
#include <obs-frontend-api.h>
#include <QAction>
#include <QDirIterator>
#include <QLibrary>
#include <QMainWindow>
#include <QTextStream>
#include <regex>
#ifdef _WIN32
#include <Windows.h>
#endif
namespace advss {
AdvSceneSwitcher *AdvSceneSwitcher::window = nullptr;
@@ -307,7 +311,7 @@ void SwitcherData::Thread()
}
}
ResetForNextInterval();
RunIntervalResetSteps();
if (match) {
if (macroMatch) {
@@ -357,14 +361,6 @@ void SwitcherData::SetPreconditions()
InvalidateMacroTempVarValues();
}
void SwitcherData::ResetForNextInterval()
{
// Plugin reset functions
for (const auto &func : resetIntervalSteps) {
func();
}
}
bool SwitcherData::CheckForMatch(OBSWeakSource &scene,
OBSWeakSource &transition, int &linger,
bool &setPrevSceneAfterLinger,
@@ -445,7 +441,7 @@ void AutoStartActionQueues();
void SwitcherData::Start()
{
if (!(th && th->isRunning())) {
ResetForNextInterval();
RunIntervalResetSteps();
ResetMacros();
AutoStartActionQueues();
@@ -453,10 +449,7 @@ void SwitcherData::Start()
th = new SwitcherThread();
th->start((QThread::Priority)threadPriority);
// Will be overwritten quickly but might be useful
writeToStatusFile("Advanced Scene Switcher running");
SendWebsocketVendorEvent("AdvancedSceneSwitcherStarted",
nullptr);
RunStartSteps();
}
if (showSystemTrayNotifications) {
@@ -483,11 +476,7 @@ void SwitcherData::Stop()
th->wait();
delete th;
th = nullptr;
writeToStatusFile("Advanced Scene Switcher stopped");
if (!obsIsShuttingDown) {
SendWebsocketVendorEvent("AdvancedSceneSwitcherStopped",
nullptr);
}
RunStopSteps();
}
if (showSystemTrayNotifications) {

View File

@@ -21,6 +21,29 @@ bool FileSwitch::pause = false;
static QObject *addPulse = nullptr;
static std::hash<std::string> strHash;
static void writeToStatusFile(const QString &msg)
{
if (!GetSwitcher() || !GetSwitcher()->fileIO.writeEnabled ||
GetSwitcher()->fileIO.writePath.empty()) {
return;
}
QFile file(QString::fromStdString(GetSwitcher()->fileIO.writePath));
if (file.open(QIODevice::ReadWrite)) {
QTextStream stream(&file);
stream << msg << Qt::endl;
}
file.close();
}
static bool _ = []() {
AddStartStep(
[]() { writeToStatusFile("Advanced Scene Switcher running"); });
AddStopStep(
[]() { writeToStatusFile("Advanced Scene Switcher stopped"); });
return true;
}();
void AdvSceneSwitcher::on_browseButton_clicked()
{
QString path = QFileDialog::getOpenFileName(
@@ -113,20 +136,6 @@ void SwitcherData::writeSceneInfoToFile()
}
}
void SwitcherData::writeToStatusFile(const QString &msg)
{
if (!fileIO.writeEnabled || fileIO.writePath.empty()) {
return;
}
QFile file(QString::fromStdString(fileIO.writePath));
if (file.open(QIODevice::ReadWrite)) {
QTextStream stream(&file);
stream << msg << Qt::endl;
}
file.close();
}
bool SwitcherData::checkSwitchInfoFromFile(OBSWeakSource &scene,
OBSWeakSource &transition)
{

View File

@@ -550,6 +550,11 @@ static void initProc2()
#endif
}
int ignoreXerror(Display *d, XErrorEvent *e)
{
return 0;
}
void PlatformInit()
{
auto display = disp();
@@ -560,6 +565,7 @@ void PlatformInit()
initXss();
initProcps();
initProc2();
XSetErrorHandler(ignoreXerror);
}
static void cleanupHelper(QLibrary *lib)
@@ -576,6 +582,7 @@ void PlatformCleanup()
cleanupHelper(libprocps);
cleanupHelper(libproc2);
cleanupDisplay();
XSetErrorHandler(NULL);
}
} // namespace advss

View File

@@ -7,8 +7,6 @@
#include "section.hpp"
#include "switch-button.hpp"
#include <QGraphicsOpacityEffect>
namespace advss {
static inline void populateActionSelection(QComboBox *list)
@@ -17,6 +15,9 @@ static inline void populateActionSelection(QComboBox *list)
QString entry(obs_module_text(action._name.c_str()));
if (list->findText(entry) == -1) {
list->addItem(entry);
qobject_cast<QListView *>(list->view())
->setRowHidden(list->count() - 1,
action._hidden);
} else {
blog(LOG_WARNING,
"did not insert duplicate action entry with name \"%s\"",
@@ -116,17 +117,6 @@ void MacroActionEdit::SetEntryData(std::shared_ptr<MacroAction> *data)
_entryData = data;
}
void MacroActionEdit::SetDisableEffect(bool value)
{
if (value) {
auto effect = new QGraphicsOpacityEffect(this);
effect->setOpacity(0.5);
_section->setGraphicsEffect(effect);
} else {
_section->setGraphicsEffect(nullptr);
}
}
void MacroActionEdit::ActionEnableChanged(bool value)
{
if (_loading || !_entryData) {
@@ -144,13 +134,9 @@ void MacroActionEdit::UpdateActionState()
return;
}
SetEnableAppearance((*_entryData)->Enabled());
}
void MacroActionEdit::SetEnableAppearance(bool value)
{
_enable->setChecked(value);
SetDisableEffect(!value);
const bool enabled = (*_entryData)->Enabled();
SetEnableAppearance(enabled);
_enable->setChecked(enabled);
}
std::shared_ptr<MacroSegment> MacroActionEdit::Data() const

View File

@@ -27,8 +27,6 @@ private slots:
private:
std::shared_ptr<MacroSegment> Data() const;
void SetDisableEffect(bool);
void SetEnableAppearance(bool);
FilterComboBox *_actionSelection;
SwitchButton *_enable;

View File

@@ -12,6 +12,7 @@ struct MacroActionInfo {
std::function<std::shared_ptr<MacroAction>(Macro *m)> _create = nullptr;
CreateActionWidget _createWidget = nullptr;
std::string _name;
bool _hidden = false;
};
class MacroActionFactory {

View File

@@ -11,15 +11,12 @@ bool MacroAction::Save(obs_data_t *obj) const
{
MacroSegment::Save(obj);
obs_data_set_string(obj, "id", GetId().c_str());
obs_data_set_bool(obj, "enabled", _enabled);
return true;
}
bool MacroAction::Load(obs_data_t *obj)
{
MacroSegment::Load(obj);
obs_data_set_default_bool(obj, "enabled", true);
_enabled = obs_data_get_bool(obj, "enabled");
return true;
}
@@ -28,16 +25,6 @@ void MacroAction::LogAction() const
ablog(LOG_INFO, "performed action %s", GetId().c_str());
}
void MacroAction::SetEnabled(bool value)
{
_enabled = value;
}
bool MacroAction::Enabled() const
{
return _enabled;
}
void MacroAction::ResolveVariablesToFixedValues() {}
std::string_view MacroAction::GetDefaultID()

View File

@@ -19,13 +19,9 @@ public:
// Used to resolve variables before actions are added to action queues
virtual void ResolveVariablesToFixedValues();
void SetEnabled(bool);
bool Enabled() const;
static std::string_view GetDefaultID();
private:
bool _enabled = true;
};
class EXPORT MacroRefAction : virtual public MacroAction {

View File

@@ -152,6 +152,8 @@ void MacroConditionEdit::LogicSelectionChanged(int idx)
const auto logic = static_cast<Logic::Type>(
_logicSelection->itemData(idx).toInt());
(*_entryData)->SetLogicType(logic);
SetEnableAppearance(logic != Logic::Type::NONE);
}
bool MacroConditionEdit::IsRootNode()
@@ -164,6 +166,7 @@ void MacroConditionEdit::SetLogicSelection()
const auto logic = (*_entryData)->GetLogicType();
_logicSelection->setCurrentIndex(
_logicSelection->findData(static_cast<int>(logic)));
SetEnableAppearance(logic != Logic::Type::NONE);
}
void MacroConditionEdit::SetRootNode(bool root)

View File

@@ -9,7 +9,6 @@
#include <QPushButton>
#include <QHBoxLayout>
#include <QTimer>
#include <QListWidget>
namespace advss {

View File

@@ -67,9 +67,7 @@ void SetMacroSwitchedScene(bool value)
{
static bool setupDone = false;
if (!setupDone) {
// Will always be called with switcher lock already held
AddIntervalResetStep([]() { macroSceneSwitched = false; },
false);
AddIntervalResetStep([]() { macroSceneSwitched = false; });
setupDone = true;
}
macroSceneSwitched = value;

View File

@@ -108,6 +108,24 @@ static const std::string setTempVarValueDeclString =
tempVarIdParam.data() + ", in string " + valueParam.data() +
", in int " + GetInstanceIdParamName().data() + ")";
/* Plugin status */
static constexpr std::string_view stopSignalName = "advss_plugin_stopped";
static constexpr std::string_view startSignalName = "advss_plugin_started";
static constexpr std::string_view getRunningStatusFuncName =
"advss_plugin_running";
static constexpr std::string_view resetIntervalSignalName =
"advss_interval_reset";
static const std::string stopSignalDeclString =
std::string("void ") + stopSignalName.data() + "()";
static const std::string startSignalDeclString =
std::string("void ") + startSignalName.data() + "()";
static const std::string resetIntervalDeclString =
std::string("void ") + resetIntervalSignalName.data() + "()";
static const std::string getRunningStatusDeclString =
std::string("bool ") + getRunningStatusFuncName.data() + "()";
static bool setup();
static bool setupDone = setup();
@@ -134,6 +152,25 @@ static bool setup()
&ScriptHandler::DeregisterAllTempVars, nullptr);
proc_handler_add(ph, setTempVarValueDeclString.c_str(),
&ScriptHandler::SetTempVarValue, nullptr);
proc_handler_add(ph, getRunningStatusDeclString.c_str(),
&ScriptHandler::GetRunningStatus, nullptr);
auto sh = obs_get_signal_handler();
signal_handler_add(sh, stopSignalDeclString.c_str());
signal_handler_add(sh, startSignalDeclString.c_str());
signal_handler_add(sh, resetIntervalDeclString.c_str());
static constexpr auto triggerSignal = [](const std::string_view &name) {
auto sh = obs_get_signal_handler();
struct calldata data;
calldata_init(&data);
signal_handler_signal(sh, name.data(), &data);
calldata_free(&data);
};
AddIntervalResetStep([]() { triggerSignal(resetIntervalSignalName); });
AddStartStep([]() { triggerSignal(startSignalName); });
AddStopStep([]() { triggerSignal(stopSignalName); });
return true;
}
@@ -476,6 +513,11 @@ void ScriptHandler::SetVariableValue(void *, calldata_t *data)
RETURN_SUCCESS();
}
void ScriptHandler::GetRunningStatus(void *ctx, calldata_t *data)
{
calldata_set_bool(data, "is_running", PluginIsRunning());
}
void ScriptHandler::RegisterTempVar(void *, calldata_t *data)
{
const char *variableId;

View File

@@ -32,6 +32,7 @@ public:
static void DeregisterAllTempVars(void *ctx, calldata_t *data);
static void SetTempVarValue(void *ctx, calldata_t *data);
static void SetVariableValue(void *ctx, calldata_t *data);
static void GetRunningStatus(void *ctx, calldata_t *data);
static bool ActionIdIsValid(const std::string &id);
static bool ConditionIdIsValid(const std::string &id);

View File

@@ -6,6 +6,7 @@
#include <QApplication>
#include <QEvent>
#include <QGraphicsOpacityEffect>
#include <QLabel>
#include <QMouseEvent>
#include <QScrollBar>
@@ -24,24 +25,24 @@ bool MacroSegment::Save(obs_data_t *obj) const
obs_data_set_bool(data, "collapsed", _collapsed);
obs_data_set_bool(data, "useCustomLabel", _useCustomLabel);
obs_data_set_string(data, "customLabel", _customLabel.c_str());
obs_data_set_bool(data, "enabled", _enabled);
obs_data_set_int(data, "version", 1);
obs_data_set_obj(obj, "segmentSettings", data);
return true;
}
bool MacroSegment::Load(obs_data_t *obj)
{
OBSDataAutoRelease data = obs_data_get_obj(obj, "segmentSettings");
_collapsed = obs_data_get_bool(data, "collapsed");
_useCustomLabel = obs_data_get_bool(data, "useCustomLabel");
_customLabel = obs_data_get_string(data, "customLabel");
obs_data_set_default_bool(data, "enabled", true);
_enabled = obs_data_get_bool(data, "enabled");
// TODO: remove this fallback at some point
if (obs_data_has_user_value(obj, "segmentSettings")) {
OBSDataAutoRelease data =
obs_data_get_obj(obj, "segmentSettings");
_collapsed = obs_data_get_bool(data, "collapsed");
_useCustomLabel = obs_data_get_bool(data, "useCustomLabel");
_customLabel = obs_data_get_string(data, "customLabel");
} else {
_collapsed = obs_data_get_bool(obj, "collapsed");
_useCustomLabel = false;
_customLabel = obs_module_text(
"AdvSceneSwitcher.macroTab.segment.defaultCustomLabel");
if (!obs_data_has_user_value(data, "version")) {
_enabled = obs_data_get_bool(obj, "enabled");
}
ClearAvailableTempvars();
@@ -73,6 +74,16 @@ bool MacroSegment::GetHighlightAndReset()
return false;
}
void MacroSegment::SetEnabled(bool value)
{
_enabled = value;
}
bool MacroSegment::Enabled() const
{
return _enabled;
}
std::string MacroSegment::GetVariableValue() const
{
if (_supportsVariableValue) {
@@ -320,6 +331,22 @@ void MacroSegmentEdit::Collapsed(bool collapsed)
}
}
void MacroSegmentEdit::SetDisableEffect(bool value)
{
if (value) {
auto effect = new QGraphicsOpacityEffect(this);
effect->setOpacity(0.5);
_section->setGraphicsEffect(effect);
} else {
_section->setGraphicsEffect(nullptr);
}
}
void MacroSegmentEdit::SetEnableAppearance(bool value)
{
SetDisableEffect(!value);
}
void MacroSegmentEdit::SetFocusPolicyOfWidgets()
{
QList<QWidget *> widgets = this->findChildren<QWidget *>();

View File

@@ -39,6 +39,8 @@ public:
virtual std::string GetId() const = 0;
void EnableHighlight();
bool GetHighlightAndReset();
void SetEnabled(bool);
bool Enabled() const;
virtual std::string GetVariableValue() const;
protected:
@@ -75,6 +77,7 @@ private:
// UI helper
bool _highlight = false;
bool _collapsed = false;
bool _enabled = true;
// Custom header labels
bool _useCustomLabel = false;
@@ -118,6 +121,9 @@ signals:
void SceneGroupRenamed(const QString &oldName, const QString newName);
protected:
void SetDisableEffect(bool);
void SetEnableAppearance(bool);
bool eventFilter(QObject *obj, QEvent *ev) override;
Section *_section;

View File

@@ -168,17 +168,6 @@ void SwitcherData::SaveVersion(obs_data_t *obj,
obs_data_set_string(obj, "version", currentVersion.c_str());
}
void SwitcherData::AddIntervalResetStep(std::function<void()> function,
bool tryLock)
{
if (!tryLock) {
resetIntervalSteps.emplace_back(function);
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
resetIntervalSteps.emplace_back(function);
}
void SwitcherData::RunPostLoadSteps()
{
for (const auto &func : postLoadSteps) {

View File

@@ -57,11 +57,9 @@ public:
bool AnySceneTransitionStarted();
void SetPreconditions();
void ResetForNextInterval();
void AddSaveStep(std::function<void(obs_data_t *)>);
void AddLoadStep(std::function<void(obs_data_t *)>);
void AddPostLoadStep(std::function<void()>);
void AddIntervalResetStep(std::function<void()>, bool lock = true);
void RunPostLoadSteps();
bool CheckForMatch(OBSWeakSource &scene, OBSWeakSource &transition,
int &linger, bool &setPreviousSceneAsMatch,
@@ -100,7 +98,6 @@ public:
std::vector<std::function<void(obs_data_t *)>> saveSteps;
std::vector<std::function<void(obs_data_t *)>> loadSteps;
std::vector<std::function<void()>> postLoadSteps;
std::vector<std::function<void()>> resetIntervalSteps;
bool firstBoot = true;
bool transitionActive = false;
@@ -233,7 +230,6 @@ public:
bool checkPause();
void checkDefaultSceneTransitions();
void writeSceneInfoToFile();
void writeToStatusFile(const QString &msg);
void checkSwitchCooldown(bool &match);
std::deque<WindowSwitch> windowSwitches;

View File

@@ -32,7 +32,9 @@ void PreventMouseWheelAdjustWithoutFocus(QWidget *w)
QString(w->metaObject()->className()) ==
"advss::OSCMessageElementEdit" ||
QString(w->metaObject()->className()) ==
"advss::ChatMessagePropertyEdit") {
"advss::ChatMessagePropertyEdit" ||
QString(w->metaObject()->className()) ==
"advss::KeyValueListContainerWidget") {
return;
}
w->setFocusPolicy(Qt::StrongFocus);

View File

@@ -67,6 +67,8 @@ NonModalMessageDialog::NonModalMessageDialog(const QString &message, Type type,
}
case Type::INPUT: {
_inputEdit = new ResizingPlainTextEdit(this);
_inputEdit->setTabChangesFocus(true);
_inputEdit->setFocus();
connect(_inputEdit, &ResizingPlainTextEdit::textChanged, this,
&NonModalMessageDialog::InputChanged);
layout->addWidget(_inputEdit);

View File

@@ -21,6 +21,24 @@ static std::vector<std::function<void()>> &getPluginCleanupSteps()
return steps;
}
static std::vector<std::function<void()>> &getResetIntervalSteps()
{
static std::vector<std::function<void()>> steps;
return steps;
}
static std::vector<std::function<void()>> &getStartSteps()
{
static std::vector<std::function<void()>> steps;
return steps;
}
static std::vector<std::function<void()>> &getStopSteps()
{
static std::vector<std::function<void()>> steps;
return steps;
}
static std::mutex mutex;
void SavePluginSettings(obs_data_t *obj)
@@ -48,9 +66,10 @@ void AddPostLoadStep(std::function<void()> step)
GetSwitcher()->AddPostLoadStep(step);
}
void AddIntervalResetStep(std::function<void()> step, bool lock)
void AddIntervalResetStep(std::function<void()> step)
{
GetSwitcher()->AddIntervalResetStep(step, lock);
std::lock_guard<std::mutex> lock(mutex);
getResetIntervalSteps().emplace_back(step);
}
void RunPostLoadSteps()
@@ -100,6 +119,42 @@ void RunPluginCleanupSteps()
}
}
void RunIntervalResetSteps()
{
std::lock_guard<std::mutex> lock(mutex);
for (const auto &step : getResetIntervalSteps()) {
step();
}
}
void AddStartStep(std::function<void()> step)
{
std::lock_guard<std::mutex> lock(mutex);
getStartSteps().emplace_back(step);
}
void AddStopStep(std::function<void()> step)
{
std::lock_guard<std::mutex> lock(mutex);
getStopSteps().emplace_back(step);
}
void RunStartSteps()
{
std::lock_guard<std::mutex> lock(mutex);
for (const auto &step : getStartSteps()) {
step();
}
}
void RunStopSteps()
{
std::lock_guard<std::mutex> lock(mutex);
for (const auto &step : getStopSteps()) {
step();
}
}
void StopPlugin()
{
GetSwitcher()->Stop();

View File

@@ -11,7 +11,7 @@ EXPORT void LoadPluginSettings(obs_data_t *);
EXPORT void AddSaveStep(std::function<void(obs_data_t *)>);
EXPORT void AddLoadStep(std::function<void(obs_data_t *)>);
EXPORT void AddPostLoadStep(std::function<void()>);
EXPORT void AddIntervalResetStep(std::function<void()>, bool lock = true);
EXPORT void AddIntervalResetStep(std::function<void()>);
EXPORT void RunPostLoadSteps();
EXPORT void AddPluginInitStep(std::function<void()>);
@@ -25,6 +25,11 @@ EXPORT void StopPlugin();
EXPORT void StartPlugin();
EXPORT bool PluginIsRunning();
EXPORT int GetIntervalValue();
void AddStartStep(std::function<void()>);
void AddStopStep(std::function<void()>);
void RunStartSteps();
void RunStopSteps();
void RunIntervalResetSteps();
enum class NoMatchBehavior { NO_SWITCH = 0, SWITCH = 1, RANDOM_SWITCH = 2 };
EXPORT void SetPluginNoMatchBehavior(NoMatchBehavior);

View File

@@ -23,6 +23,14 @@ static bool setupDone = setup();
bool setup()
{
AddPluginPostLoadStep(registerWebsocketVendor);
AddStartStep([]() {
SendWebsocketVendorEvent("AdvancedSceneSwitcherStarted",
nullptr);
});
AddStopStep([]() {
SendWebsocketVendorEvent("AdvancedSceneSwitcherStopped",
nullptr);
});
return true;
}
@@ -109,6 +117,9 @@ void RegisterWebsocketRequest(
void SendWebsocketVendorEvent(const std::string &eventName, obs_data_t *data)
{
if (OBSIsShuttingDown()) {
return;
}
obs_websocket_vendor_emit_event(vendor, eventName.c_str(), data);
}

View File

@@ -28,6 +28,7 @@ install_advss_plugin_dependency(...)
... to install the plugin and its dependencies.
#]]
add_plugin(http)
add_plugin(midi)
add_plugin(openvr)
add_plugin(stream-deck)

View File

@@ -15,8 +15,8 @@ target_sources(
macro-action-filter.hpp
macro-action-hotkey.cpp
macro-action-hotkey.hpp
macro-action-http.cpp
macro-action-http.hpp
macro-action-http-legacy.cpp
macro-action-http-legacy.hpp
macro-action-log.cpp
macro-action-log.hpp
macro-action-media.cpp

View File

@@ -449,8 +449,11 @@ void MacroActionFilterEdit::SetWidgetVisibility()
_entryData->_action == MacroActionFilter::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionFilter::SettingsInputMethod::JSON_STRING);
_getSettings->setVisible(_entryData->_action ==
MacroActionFilter::Action::SETTINGS);
_getSettings->setVisible(
_entryData->_action == MacroActionFilter::Action::SETTINGS &&
_entryData->_settingsInputMethod !=
MacroActionFilter::SettingsInputMethod::
INDIVIDUAL_TEMPVAR);
_tempVars->setVisible(_entryData->_action ==
MacroActionFilter::Action::SETTINGS &&
_entryData->_settingsInputMethod ==

View File

@@ -1,4 +1,4 @@
#include "macro-action-http.hpp"
#include "macro-action-http-legacy.hpp"
#include "curl-helper.hpp"
#include "layout-helpers.hpp"
@@ -9,7 +9,7 @@ const std::string MacroActionHttp::id = "http";
bool MacroActionHttp::_registered = MacroActionFactory::Register(
MacroActionHttp::id,
{MacroActionHttp::Create, MacroActionHttpEdit::Create,
"AdvSceneSwitcher.action.http"});
"AdvSceneSwitcher.action.http.legacy", true});
const static std::map<MacroActionHttp::Method, std::string> methods = {
{MacroActionHttp::Method::GET, "AdvSceneSwitcher.action.http.type.get"},

View File

@@ -34,11 +34,11 @@ const static std::map<PluginStateAction, std::string> actionTypes = {
const static std::map<NoMatchBehavior, std::string> noMatchValues = {
{NoMatchBehavior::NO_SWITCH,
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMet.dontSwitch"},
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMatch.dontSwitch"},
{NoMatchBehavior::SWITCH,
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMet.switchTo"},
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMatch.switchTo"},
{NoMatchBehavior::RANDOM_SWITCH,
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMet.switchToRandom"},
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMatch.switchToRandom"},
};
static void stopPlugin()

View File

@@ -5,6 +5,8 @@
#include "source-helpers.hpp"
#include <obs-frontend-api.h>
#include <QApplication>
#include <QWindow>
namespace advss {
@@ -28,8 +30,34 @@ const static std::map<MacroActionProjector::Type, std::string> selectionTypes =
"AdvSceneSwitcher.action.projector.type.multiview"},
};
static void closeOBSProjectorWindows(const std::string expectedWindowTitle,
const RegexConfig &regex)
{
for (QWindow *widget : QApplication::allWindows()) {
if (!widget->property("isOBSProjectorWindow").toBool()) {
continue;
}
auto const windowTitle = widget->title().toStdString();
if (!regex.Enabled() && expectedWindowTitle != windowTitle) {
continue;
}
if (!regex.Matches(windowTitle, expectedWindowTitle)) {
continue;
}
widget->close();
}
}
bool MacroActionProjector::PerformAction()
{
if (_action == Action::CLOSE) {
closeOBSProjectorWindows(_projectorWindowName, _regex);
return true;
}
std::string name = "";
const char *type = "";
@@ -78,10 +106,16 @@ bool MacroActionProjector::PerformAction()
void MacroActionProjector::LogAction() const
{
if (_action == Action::CLOSE) {
ablog(LOG_INFO, "closing projector window \"%s\"",
_projectorWindowName.c_str());
return;
}
auto it = selectionTypes.find(_type);
if (it != selectionTypes.end()) {
ablog(LOG_INFO,
"performed projector action \"%s\" with"
"open projector \"%s\" with"
"source \"%s\","
"scene \"%s\","
"monitor %d",
@@ -96,24 +130,30 @@ void MacroActionProjector::LogAction() const
bool MacroActionProjector::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
obs_data_set_int(obj, "action", static_cast<int>(_action));
obs_data_set_int(obj, "type", static_cast<int>(_type));
obs_data_set_int(obj, "monitor", _monitor);
obs_data_set_string(obj, "monitorName", _monitorName.c_str());
obs_data_set_bool(obj, "fullscreen", _fullscreen);
_scene.Save(obj);
_source.Save(obj);
_projectorWindowName.Save(obj, "projectorWindowName");
_regex.Save(obj);
return true;
}
bool MacroActionProjector::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
_type = static_cast<Type>(obs_data_get_int(obj, "type"));
_monitor = obs_data_get_int(obj, "monitor");
_monitorName = obs_data_get_string(obj, "monitorName");
_fullscreen = obs_data_get_bool(obj, "fullscreen");
_scene.Load(obj);
_source.Load(obj);
_projectorWindowName.Load(obj, "projectorWindowName");
_regex.Load(obj);
return true;
}
@@ -131,6 +171,7 @@ void MacroActionProjector::ResolveVariablesToFixedValues()
{
_source.ResolveVariables();
_scene.ResolveVariables();
_projectorWindowName.ResolveVariables();
}
void MacroActionProjector::SetMonitor(int idx)
@@ -166,14 +207,22 @@ bool MacroActionProjector::MonitorSetupChanged() const
QString::fromStdString(_monitorName);
}
static inline void populateSelectionTypes(QComboBox *list)
static void populateActionSelection(QComboBox *list)
{
for (auto entry : selectionTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
list->addItem(obs_module_text(
"AdvSceneSwitcher.action.projector.action.open"));
list->addItem(obs_module_text(
"AdvSceneSwitcher.action.projector.action.close"));
}
static void populateSelectionTypes(QComboBox *list)
{
for (const auto &[_, name] : selectionTypes) {
list->addItem(obs_module_text(name.c_str()));
}
}
static inline void populateWindowTypes(QComboBox *list)
static void populateWindowTypes(QComboBox *list)
{
list->addItem(
obs_module_text("AdvSceneSwitcher.action.projector.windowed"));
@@ -184,14 +233,18 @@ static inline void populateWindowTypes(QComboBox *list)
MacroActionProjectorEdit::MacroActionProjectorEdit(
QWidget *parent, std::shared_ptr<MacroActionProjector> entryData)
: QWidget(parent),
_windowTypes(new QComboBox()),
_actions(new QComboBox()),
_types(new QComboBox()),
_windowTypes(new QComboBox()),
_scenes(new SceneSelectionWidget(window(), true, false, true, true,
true)),
_sources(new SourceSelectionWidget(window(), QStringList(), true)),
_monitorSelection(new QHBoxLayout()),
_monitors(new QComboBox())
_monitors(new QComboBox()),
_projectorWindowName(new VariableLineEdit(this)),
_regex(new RegexConfigWidget(this)),
_layout(new QHBoxLayout(this))
{
populateActionSelection(_actions);
populateWindowTypes(_windowTypes);
populateSelectionTypes(_types);
auto sources = GetSourceNames();
@@ -201,6 +254,8 @@ MacroActionProjectorEdit::MacroActionProjectorEdit(
_monitors->setPlaceholderText(
obs_module_text("AdvSceneSwitcher.selectDisplay"));
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionChanged(int)));
QWidget::connect(_windowTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(WindowTypeChanged(int)));
QWidget::connect(_types, SIGNAL(currentIndexChanged(int)), this,
@@ -212,24 +267,15 @@ MacroActionProjectorEdit::MacroActionProjectorEdit(
SLOT(SourceChanged(const SourceSelection &)));
QWidget::connect(_monitors, SIGNAL(currentIndexChanged(int)), this,
SLOT(MonitorChanged(int)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{windowTypes}}", _windowTypes}, {"{{types}}", _types},
{"{{scenes}}", _scenes}, {"{{sources}}", _sources},
{"{{monitors}}", _monitors},
};
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.projector.entry.monitor"),
_monitorSelection, widgetPlaceholders);
QHBoxLayout *mainLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.projector.entry"),
mainLayout, widgetPlaceholders);
mainLayout->insertLayout(mainLayout->count() - 1, _monitorSelection);
setLayout(mainLayout);
QWidget::connect(_projectorWindowName, SIGNAL(editingFinished()), this,
SLOT(ProjectorWindowNameChanged()));
QWidget::connect(_regex,
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
SLOT(RegexChanged(const RegexConfig &)));
_entryData = entryData;
SetWidgetLayout();
setLayout(_layout);
UpdateEntryData();
_loading = false;
}
@@ -239,11 +285,15 @@ void MacroActionProjectorEdit::UpdateEntryData()
if (!_entryData) {
return;
}
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_windowTypes->setCurrentIndex(_entryData->_fullscreen ? 1 : 0);
_types->setCurrentIndex(static_cast<int>(_entryData->_type));
_scenes->SetScene(_entryData->_scene);
_sources->SetSource(_entryData->_source);
_monitors->setCurrentIndex(_entryData->GetMonitor());
_projectorWindowName->setText(_entryData->_projectorWindowName);
_regex->SetRegexConfig(_entryData->_regex);
SetWidgetVisibility();
}
@@ -265,12 +315,26 @@ void MacroActionProjectorEdit::MonitorChanged(int value)
_entryData->SetMonitor(value);
}
void MacroActionProjectorEdit::ProjectorWindowNameChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_projectorWindowName =
_projectorWindowName->text().toStdString();
}
void MacroActionProjectorEdit::RegexChanged(const RegexConfig &regex)
{
GUARD_LOADING_AND_LOCK();
_entryData->_regex = regex;
}
void MacroActionProjectorEdit::WindowTypeChanged(int)
{
GUARD_LOADING_AND_LOCK();
_entryData->_fullscreen =
_windowTypes->currentText() ==
obs_module_text("AdvSceneSwitcher.action.projector.fullscreen");
SetWidgetLayout();
SetWidgetVisibility();
}
@@ -281,17 +345,69 @@ void MacroActionProjectorEdit::TypeChanged(int value)
SetWidgetVisibility();
}
void MacroActionProjectorEdit::ActionChanged(int idx)
{
GUARD_LOADING_AND_LOCK();
_entryData->_action = static_cast<MacroActionProjector::Action>(idx);
SetWidgetLayout();
SetWidgetVisibility();
}
void MacroActionProjectorEdit::SetWidgetLayout()
{
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{actions}}", _actions},
{"{{windowTypes}}", _windowTypes},
{"{{types}}", _types},
{"{{scenes}}", _scenes},
{"{{sources}}", _sources},
{"{{monitors}}", _monitors},
{"{{projectorWindowName}}", _projectorWindowName},
{"{{regex}}", _regex},
};
for (const auto &[_, widget] : widgetPlaceholders) {
_layout->removeWidget(widget);
}
ClearLayout(_layout);
const char *layoutText;
if (_entryData->_action == MacroActionProjector::Action::CLOSE) {
layoutText = "AdvSceneSwitcher.action.projector.entry.close";
} else if (_entryData->_fullscreen) {
layoutText =
"AdvSceneSwitcher.action.projector.entry.open.fullscreen";
} else {
layoutText =
"AdvSceneSwitcher.action.projector.entry.open.windowed";
}
PlaceWidgets(obs_module_text(layoutText), _layout, widgetPlaceholders);
}
void MacroActionProjectorEdit::SetWidgetVisibility()
{
if (!_entryData) {
return;
}
_scenes->setVisible(_entryData->_type ==
MacroActionProjector::Type::SCENE);
_sources->setVisible(_entryData->_type ==
MacroActionProjector::Type::SOURCE);
SetLayoutVisible(_monitorSelection, _entryData->_fullscreen);
_projectorWindowName->setVisible(_entryData->_action ==
MacroActionProjector::Action::CLOSE);
_regex->setVisible(_entryData->_action ==
MacroActionProjector::Action::CLOSE);
_types->setVisible(_entryData->_action ==
MacroActionProjector::Action::OPEN);
_windowTypes->setVisible(_entryData->_action ==
MacroActionProjector::Action::OPEN);
_scenes->setVisible(
_entryData->_action == MacroActionProjector::Action::OPEN &&
_entryData->_type == MacroActionProjector::Type::SCENE);
_sources->setVisible(
_entryData->_action == MacroActionProjector::Action::OPEN &&
_entryData->_type == MacroActionProjector::Type::SOURCE);
_monitors->setVisible(_entryData->_action ==
MacroActionProjector::Action::OPEN &&
_entryData->_fullscreen);
adjustSize();
updateGeometry();

View File

@@ -1,7 +1,9 @@
#pragma once
#include "macro-action-edit.hpp"
#include "regex-config.hpp"
#include "scene-selection.hpp"
#include "source-selection.hpp"
#include "variable-line-edit.hpp"
namespace advss {
@@ -19,6 +21,11 @@ public:
void SetMonitor(int);
int GetMonitor() const;
enum class Action {
OPEN,
CLOSE,
};
enum class Type {
SOURCE,
SCENE,
@@ -27,10 +34,13 @@ public:
MULTIVIEW,
};
Action _action = Action::OPEN;
Type _type = Type::SCENE;
SourceSelection _source;
SceneSelection _scene;
bool _fullscreen = true;
StringVariable _projectorWindowName = "Windowed Projector";
RegexConfig _regex = RegexConfig::PartialMatchRegexConfig(true);
private:
bool MonitorSetupChanged() const;
@@ -60,21 +70,28 @@ public:
}
private slots:
void ActionChanged(int value);
void WindowTypeChanged(int value);
void TypeChanged(int value);
void SceneChanged(const SceneSelection &);
void SourceChanged(const SourceSelection &);
void MonitorChanged(int value);
void ProjectorWindowNameChanged();
void RegexChanged(const RegexConfig &);
private:
void SetWidgetLayout();
void SetWidgetVisibility();
QComboBox *_windowTypes;
QComboBox *_actions;
QComboBox *_types;
QComboBox *_windowTypes;
SceneSelectionWidget *_scenes;
SourceSelectionWidget *_sources;
QHBoxLayout *_monitorSelection;
QComboBox *_monitors;
VariableLineEdit *_projectorWindowName;
RegexConfigWidget *_regex;
QHBoxLayout *_layout;
std::shared_ptr<MacroActionProjector> _entryData;
bool _loading = true;

View File

@@ -42,7 +42,7 @@ const static std::map<obs_deinterlace_mode, std::string> deinterlaceModes = {
{OBS_DEINTERLACE_MODE_DISABLE,
"AdvSceneSwitcher.action.source.deinterlaceMode.disable"},
{OBS_DEINTERLACE_MODE_DISCARD,
"AdvSceneSwitcher.action.source.deinterlaceMode.disable"},
"AdvSceneSwitcher.action.source.deinterlaceMode.discard"},
{OBS_DEINTERLACE_MODE_RETRO,
"AdvSceneSwitcher.action.source.deinterlaceMode.retro"},
{OBS_DEINTERLACE_MODE_BLEND,
@@ -526,8 +526,11 @@ void MacroActionSourceEdit::SetWidgetVisibility()
_entryData->_action == MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::JSON_STRING);
_getSettings->setVisible(_entryData->_action ==
MacroActionSource::Action::SETTINGS);
_getSettings->setVisible(
_entryData->_action == MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod !=
MacroActionSource::SettingsInputMethod::
INDIVIDUAL_TEMPVAR);
_tempVars->setVisible(_entryData->_action ==
MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod ==

View File

@@ -442,6 +442,13 @@ void MacroConditionFileEdit::SetWidgetVisibility()
_entryData->_onlyMatchIfChanged &&
_entryData->GetCondition() ==
MacroConditionFile::Condition::MATCH);
// TODO: Remove remote file support in future version in favor of HTTP
// action.
// Hide the option for now, if it is not used already.
_fileTypes->setVisible(_entryData->_fileType ==
MacroConditionFile::FileType::REMOTE);
adjustSize();
updateGeometry();
}

View File

@@ -41,12 +41,12 @@ public:
StringVariable _file = obs_module_text("AdvSceneSwitcher.enterPath");
StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText");
FileType _fileType = FileType::LOCAL;
RegexConfig _regex;
// TODO: Remove in future version
bool _useTime = false;
bool _onlyMatchIfChanged = false;
FileType _fileType = FileType::LOCAL;
private:
bool MatchFileContent(QString &filedata);

View File

@@ -94,11 +94,15 @@ bool MacroConditionScene::CheckCondition()
SetVariableValue(GetWeakSourceName(GetCurrentScene()));
SetTempVarValue("current",
GetWeakSourceName(GetCurrentScene()));
SetTempVarValue("previous",
GetWeakSourceName(GetPreviousScene()));
return sceneChanged;
case Type::NOT_CHANGED:
SetVariableValue(GetWeakSourceName(GetCurrentScene()));
SetTempVarValue("current",
GetWeakSourceName(GetCurrentScene()));
SetTempVarValue("previous",
GetWeakSourceName(GetPreviousScene()));
return !sceneChanged;
case Type::CURRENT_PATTERN: {
auto scene = getCurrentSceneHelper(_useTransitionTargetScene);
@@ -212,8 +216,6 @@ void MacroConditionScene::SetupTempVars()
MacroCondition::SetupTempVars();
switch (_type) {
case Type::CURRENT:
case Type::CHANGED:
case Type::NOT_CHANGED:
case Type::CURRENT_PATTERN:
AddTempvar("current",
obs_module_text(
@@ -231,6 +233,15 @@ void MacroConditionScene::SetupTempVars()
obs_module_text(
"AdvSceneSwitcher.tempVar.scene.preview"));
break;
case Type::CHANGED:
case Type::NOT_CHANGED:
AddTempvar("current",
obs_module_text(
"AdvSceneSwitcher.tempVar.scene.current"));
AddTempvar("previous",
obs_module_text(
"AdvSceneSwitcher.tempVar.scene.previous"));
break;
default:
break;
}

View File

@@ -0,0 +1,77 @@
cmake_minimum_required(VERSION 3.14)
project(advanced-scene-switcher-http)
# --- Check requirements ---
get_target_property(ADVSS_SOURCE_DIR advanced-scene-switcher-lib SOURCE_DIR)
set(CPP_HTTPLIB_DIR "${ADVSS_SOURCE_DIR}/deps/cpp-httplib")
if(NOT EXISTS "${CPP_HTTPLIB_DIR}/CMakeLists.txt")
message(WARNING "cpp-httplib directory \"${CPP_HTTPLIB_DIR}\" not found!\n"
"HTTP support will be disabled!")
return()
endif()
if(NOT TARGET httplib)
add_subdirectory("${CPP_HTTPLIB_DIR}" "${CPP_HTTPLIB_DIR}/build"
EXCLUDE_FROM_ALL)
endif()
if(NOT OPENSSL_INCLUDE_DIR OR NOT OPENSSL_LIBRARIES)
find_package(OpenSSL)
if(NOT OPENSSL_FOUND)
message(WARNING "OpenSSL not found!\n" "HTTP support will be disabled!\n\n")
return()
endif()
endif()
find_package(ZLIB)
if(NOT ZLIB_FOUND)
message(WARNING "zlib not found!\n" "HTTP support will be disabled!\n\n")
return()
endif()
# --- End of section ---
add_library(${PROJECT_NAME} MODULE)
target_compile_definitions(${PROJECT_NAME} PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT=1)
if(OS_MACOS)
target_compile_definitions(
${PROJECT_NAME} PRIVATE CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN=1)
target_link_libraries(${PROJECT_NAME} PRIVATE "-framework CoreFoundation")
target_link_libraries(${PROJECT_NAME} PRIVATE "-framework Security")
endif()
target_sources(
${PROJECT_NAME} PRIVATE macro-action-http.cpp macro-action-http.hpp
key-value-list.cpp key-value-list.hpp)
setup_advss_plugin(${PROJECT_NAME})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
target_include_directories(${PROJECT_NAME} PRIVATE "${CPP_HTTPLIB_DIR}/"
"${OPENSSL_INCLUDE_DIR}")
target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES} ZLIB::ZLIB)
install_advss_plugin(${PROJECT_NAME})
if(OS_WINDOWS)
# Couldn't really find a better way to install runtime dependencies for
# Windows TODO: Clean this up at some point
function(FIND_FILES_WITH_PATTERN result pattern dir)
execute_process(
COMMAND
powershell -Command
"Get-ChildItem -Path '${dir}' -Recurse -Include ${pattern} |"
"Select-Object -First 1 |"
"ForEach-Object { $_.FullName -replace '\\\\', '\\\\' }"
OUTPUT_VARIABLE files
OUTPUT_STRIP_TRAILING_WHITESPACE)
set(${result}
${files}
PARENT_SCOPE)
endfunction()
set(OPENSSL_DLL_SEARCH_DIR "${OPENSSL_INCLUDE_DIR}/..")
find_files_with_pattern(CRYPTO_DLL_FILES "libcrypto*.dll"
"${OPENSSL_DLL_SEARCH_DIR}")
find_files_with_pattern(SSL_DLL_FILES "libssl*.dll"
"${OPENSSL_DLL_SEARCH_DIR}")
install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES
"${CRYPTO_DLL_FILES}" "${SSL_DLL_FILES}")
endif()

View File

@@ -0,0 +1,213 @@
#include "key-value-list.hpp"
#include "name-dialog.hpp"
#include "ui-helpers.hpp"
#include <QLayout>
#include <QTimer>
namespace advss {
KeyValueListEdit::KeyValueListEdit(QWidget *parent, const QString &addKeyString,
const QString &addKeyStringDescription,
const QString &addValueString,
const QString &addValueStringDescription)
: ListEditor(parent),
_addKeyString(addKeyString),
_addKeyStringDescription(addKeyStringDescription),
_addValueString(addValueString),
_addValueStringDescription(addValueStringDescription)
{
_list->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_list->setAutoScroll(false);
}
void KeyValueListEdit::SetStringList(const StringList &list)
{
_stringList = list;
_list->clear();
for (int i = 0; i < list.size(); i += 2) {
AppendListEntryWidget(
list.at(i), i + 1 >= list.size() ? "" : list.at(i + 1));
}
UpdateListSize();
}
void KeyValueListEdit::Add()
{
StringVariable key;
StringVariable value;
bool accepted = AskForKeyValue(key, value);
if (!accepted) {
return;
}
AppendListEntryWidget(key, value);
_stringList << key << value;
// Delay resizing to make sure the list viewport was already updated
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
StringListChanged(_stringList);
}
void KeyValueListEdit::Remove()
{
int idx = _list->currentRow();
if (idx == -1) {
return;
}
_stringList.removeAt(idx);
QListWidgetItem *item = _list->currentItem();
if (!item) {
return;
}
delete item;
// Delay resizing to make sure the list viewport was already updated
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
StringListChanged(_stringList);
}
void KeyValueListEdit::Up()
{
int idx = _list->currentRow();
if (idx <= 0 || idx >= _list->count()) {
return;
}
MoveStringListIdxUp(idx);
auto row = _list->itemWidget(_list->currentItem());
auto newItem = _list->currentItem()->clone();
_list->insertItem(idx - 1, newItem);
_list->setItemWidget(newItem, row);
_list->takeItem(idx + 1);
_list->setCurrentRow(idx - 1);
UpdateListSize();
StringListChanged(_stringList);
}
void KeyValueListEdit::Down()
{
int idx = _list->currentRow();
if (idx == -1 || idx == _list->count() - 1) {
return;
}
MoveStringListIdxUp(idx + 1);
auto row = _list->itemWidget(_list->currentItem());
auto newItem = _list->currentItem()->clone();
_list->insertItem(idx + 2, newItem);
_list->setItemWidget(newItem, row);
_list->takeItem(idx);
_list->setCurrentRow(idx + 1);
UpdateListSize();
StringListChanged(_stringList);
}
void KeyValueListEdit::Clicked(QListWidgetItem *item)
{
int idx = _list->currentRow();
StringVariable key = _stringList[idx * 2];
StringVariable value = _stringList[idx * 2 + 1];
bool accepted = AskForKeyValue(key, value);
if (!accepted) {
return;
}
auto container = static_cast<KeyValueListContainerWidget *>(
_list->itemWidget(item));
container->_key->setText(QString::fromStdString(key.UnresolvedValue()));
container->_value->setText(
QString::fromStdString(value.UnresolvedValue()));
container->adjustSize();
container->updateGeometry();
_stringList[idx * 2] = key;
_stringList[idx * 2 + 1] = value;
// Delay resizing to make sure the list viewport was already updated
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
StringListChanged(_stringList);
}
void KeyValueListEdit::MoveStringListIdxUp(int idx)
{
if (idx <= 0 || idx >= _list->count()) {
return;
}
_stringList.move(idx * 2, idx * 2 - 2);
_stringList.move(idx * 2 + 1, idx * 2 - 1);
}
bool KeyValueListEdit::AskForKeyValue(StringVariable &keyVariable,
StringVariable &valueVariable)
{
std::string key;
bool accepted = NameDialog::AskForName(
this, _addKeyString, _addKeyStringDescription, key,
QString::fromStdString(keyVariable.UnresolvedValue()), 4096,
false);
if (!accepted) {
return false;
}
std::string value;
accepted = NameDialog::AskForName(
this, _addValueString, _addValueStringDescription, value,
QString::fromStdString(valueVariable.UnresolvedValue()), 4096,
false);
if (!accepted) {
return false;
}
keyVariable = key;
valueVariable = value;
return true;
}
void KeyValueListEdit::AppendListEntryWidget(const StringVariable &key,
const StringVariable &value)
{
QListWidgetItem *item = new QListWidgetItem(_list);
auto container = new KeyValueListContainerWidget(this, _list->count());
container->_key->setText(QString::fromStdString(key.UnresolvedValue()));
container->_value->setText(
QString::fromStdString(value.UnresolvedValue()));
container->adjustSize();
container->updateGeometry();
_list->addItem(item);
_list->setItemWidget(item, container);
UpdateListSize();
}
KeyValueListContainerWidget::KeyValueListContainerWidget(QWidget *parent,
int index)
: QWidget(parent),
_key(new QLabel("Key", this)),
_value(new QLabel("Value", this)),
_index(index)
{
auto layout = new QHBoxLayout();
layout->addWidget(_key);
layout->addWidget(_value);
layout->setContentsMargins(0, 0, 0, 0);
setLayout(layout);
}
} // namespace advss

View File

@@ -0,0 +1,53 @@
#pragma once
#include "string-list.hpp"
#include "variable-line-edit.hpp"
namespace advss {
class KeyValueListEdit final : public ListEditor {
Q_OBJECT
public:
KeyValueListEdit(QWidget *parent, const QString &addKeyString,
const QString &addKeyStringDescription,
const QString &addValueString,
const QString &addValueStringDescription);
void SetStringList(const StringList &);
private slots:
void Add();
void Remove();
void Up();
void Down();
void Clicked(QListWidgetItem *);
signals:
void StringListChanged(const StringList &);
private:
void MoveStringListIdxUp(int);
bool AskForKeyValue(StringVariable &key, StringVariable &value);
void AppendListEntryWidget(const StringVariable &key,
const StringVariable &value);
StringList _stringList;
QString _addKeyString;
QString _addKeyStringDescription;
QString _addValueString;
QString _addValueStringDescription;
};
class KeyValueListContainerWidget final : public QWidget {
Q_OBJECT
public:
KeyValueListContainerWidget(QWidget *parent, int index);
private:
QLabel *_key;
QLabel *_value;
int _index = -1;
friend class KeyValueListEdit;
};
} // namespace advss

View File

@@ -0,0 +1,463 @@
#include "macro-action-http.hpp"
#include "layout-helpers.hpp"
#include <httplib.h>
#undef DELETE
namespace advss {
const std::string MacroActionHttp::id = "http_v2";
bool MacroActionHttp::_registered = MacroActionFactory::Register(
MacroActionHttp::id,
{MacroActionHttp::Create, MacroActionHttpEdit::Create,
"AdvSceneSwitcher.action.http"});
static httplib::Headers getHeaders(const StringList &strings)
{
httplib::Headers headers;
for (int i = 0; i < strings.size(); i = i + 2) {
const auto pair =
i + 1 >= strings.size()
? std::make_pair(std::string(strings.at(i)), "")
: std::make_pair(
std::string(strings.at(i)),
std::string(strings.at(i + 1)));
headers.emplace(pair);
}
return headers;
}
static httplib::Params getParams(const StringList &strings)
{
httplib::Params params;
for (int i = 0; i < strings.size(); i = i + 2) {
const auto pair =
i + 1 >= strings.size()
? std::make_pair(std::string(strings.at(i)), "")
: std::make_pair(
std::string(strings.at(i)),
std::string(strings.at(i + 1)));
params.emplace(pair);
}
return params;
}
static void setTimeout(httplib::Client &client, const Duration &timeout)
{
const time_t seconds = timeout.Seconds();
const time_t usecs = timeout.Milliseconds() * 1000;
client.set_read_timeout(seconds, usecs);
client.set_write_timeout(seconds, usecs);
}
void MacroActionHttp::SetupTempVars()
{
MacroAction::SetupTempVars();
AddTempvar("status",
obs_module_text("AdvSceneSwitcher.tempVar.http.status"));
AddTempvar("body",
obs_module_text("AdvSceneSwitcher.tempVar.http.body"));
AddTempvar("error",
obs_module_text("AdvSceneSwitcher.tempVar.http.error"),
obs_module_text(
"AdvSceneSwitcher.tempVar.http.error.description"));
}
bool MacroActionHttp::PerformAction()
{
httplib::Client cli(_url);
setTimeout(cli, _timeout);
const auto params = _setParams ? getParams(_params) : httplib::Params();
const auto headers = _setHeaders ? getHeaders(_headers)
: httplib::Headers();
httplib::Result response;
switch (_method) {
case MacroActionHttp::Method::GET:
response = cli.Get(_path, params, headers);
break;
case MacroActionHttp::Method::POST: {
const auto path = httplib::append_query_params(_path, params);
response = cli.Post(path, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::PUT: {
const auto path = httplib::append_query_params(_path, params);
response = cli.Put(path, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::PATCH: {
const auto path = httplib::append_query_params(_path, params);
response = cli.Patch(path, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::DELETE: {
const auto path = httplib::append_query_params(_path, params);
response = cli.Delete(path, headers, _body, _contentType);
break;
}
default:
break;
}
SetTempVarValue("status",
response ? std::to_string(response->status) : "");
SetTempVarValue("body", response ? response->body : "");
SetTempVarValue("error",
response ? "" : httplib::to_string(response.error()));
return true;
}
static constexpr std::string_view methodToString(MacroActionHttp::Method method)
{
switch (method) {
case MacroActionHttp::Method::GET:
return "GET";
case MacroActionHttp::Method::POST:
return "POST";
case MacroActionHttp::Method::PUT:
return "PUT";
case MacroActionHttp::Method::PATCH:
return "PATCH";
case MacroActionHttp::Method::DELETE:
return "DELETE";
default:
break;
}
return "unknown";
}
static std::string stringListToString(const StringList &list)
{
if (list.empty()) {
return "[]";
}
std::string result = "[";
for (const auto &string : list) {
result += std::string(string) + ", ";
}
result.pop_back();
return result + "]";
}
void MacroActionHttp::LogAction() const
{
ablog(LOG_INFO,
"sent HTTP request (%s) "
"to URL \"%s\" "
"to path \"%s\" "
"with content type \"%s\" "
"with body \"%s\" "
"with headers \"%s\" "
"with parameters \"%s\" "
"with timeout \"%s\"",
methodToString(_method).data(), _url.c_str(), _path.c_str(),
_contentType.c_str(), _body.c_str(),
_setHeaders ? stringListToString(_headers).c_str() : "-",
_setParams ? stringListToString(_params).c_str() : "-",
_timeout.ToString().c_str());
}
bool MacroActionHttp::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
_url.Save(obj, "url");
_path.Save(obj, "path");
_contentType.Save(obj, "contentType");
_body.Save(obj, "body");
obs_data_set_bool(obj, "setHeaders", _setHeaders);
_headers.Save(obj, "headers", "header");
obs_data_set_bool(obj, "setParams", _setParams);
_params.Save(obj, "params", "param");
obs_data_set_int(obj, "method", static_cast<int>(_method));
_timeout.Save(obj);
return true;
}
bool MacroActionHttp::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_url.Load(obj, "url");
_path.Load(obj, "path");
_contentType.Load(obj, "contentType");
_body.Load(obj, "body");
_setHeaders = obs_data_get_bool(obj, "setHeaders");
_headers.Load(obj, "headers", "header");
_setParams = obs_data_get_bool(obj, "setParams");
_params.Load(obj, "params", "param");
_method = static_cast<Method>(obs_data_get_int(obj, "method"));
_timeout.Load(obj);
return true;
}
std::string MacroActionHttp::GetShortDesc() const
{
return _url.UnresolvedValue();
}
std::shared_ptr<MacroAction> MacroActionHttp::Create(Macro *m)
{
return std::make_shared<MacroActionHttp>(m);
}
std::shared_ptr<MacroAction> MacroActionHttp::Copy() const
{
return std::make_shared<MacroActionHttp>(*this);
}
void MacroActionHttp::ResolveVariablesToFixedValues()
{
_url.ResolveVariables();
_path.ResolveVariables();
_contentType.ResolveVariables();
_body.ResolveVariables();
_headers.ResolveVariables();
_params.ResolveVariables();
_timeout.ResolveVariables();
}
static inline void populateMethodSelection(QComboBox *list)
{
const static std::map<MacroActionHttp::Method, std::string> methods = {
{MacroActionHttp::Method::GET,
"AdvSceneSwitcher.action.http.type.get"},
{MacroActionHttp::Method::POST,
"AdvSceneSwitcher.action.http.type.post"},
{MacroActionHttp::Method::PUT,
"AdvSceneSwitcher.action.http.type.put"},
{MacroActionHttp::Method::PATCH,
"AdvSceneSwitcher.action.http.type.patch"},
{MacroActionHttp::Method::DELETE,
"AdvSceneSwitcher.action.http.type.delete"},
};
for (const auto &[value, name] : methods) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(value));
}
}
MacroActionHttpEdit::MacroActionHttpEdit(
QWidget *parent, std::shared_ptr<MacroActionHttp> entryData)
: QWidget(parent),
_url(new VariableLineEdit(this)),
_path(new VariableLineEdit(this)),
_contentType(new VariableLineEdit(this)),
_contentTypeLayout(new QHBoxLayout()),
_methods(new QComboBox()),
_body(new VariableTextEdit(this)),
_bodyLayout(new QVBoxLayout()),
_setHeaders(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.http.setHeaders"))),
_headerList(new KeyValueListEdit(
this, obs_module_text("AdvSceneSwitcher.action.http.headers"),
obs_module_text("AdvSceneSwitcher.action.http.addHeader.name"),
obs_module_text("AdvSceneSwitcher.action.http.headers"),
obs_module_text(
"AdvSceneSwitcher.action.http.addHeader.value"))),
_headerListLayout(new QVBoxLayout()),
_setParams(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.http.setParams"))),
_paramList(new KeyValueListEdit(
this, obs_module_text("AdvSceneSwitcher.action.http.params"),
obs_module_text("AdvSceneSwitcher.action.http.addParam.name"),
obs_module_text("AdvSceneSwitcher.action.http.params"),
obs_module_text(
"AdvSceneSwitcher.action.http.addParam.value"))),
_paramListLayout(new QVBoxLayout()),
_timeout(new DurationSelection(this, false))
{
populateMethodSelection(_methods);
SetWidgetSignalConnections();
SetWidgetLayout();
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionHttpEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_url->setText(_entryData->_url);
_path->setText(_entryData->_path);
_contentType->setText(_entryData->_contentType);
_body->setPlainText(_entryData->_body);
_setHeaders->setChecked(_entryData->_setHeaders);
_headerList->SetStringList(_entryData->_headers);
_setParams->setChecked(_entryData->_setParams);
_paramList->SetStringList(_entryData->_params);
_methods->setCurrentIndex(
_methods->findData(static_cast<int>(_entryData->_method)));
_timeout->SetDuration(_entryData->_timeout);
SetWidgetVisibility();
}
void MacroActionHttpEdit::URLChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_url = _url->text().toStdString();
emit(HeaderInfoChanged(_url->text()));
}
void MacroActionHttpEdit::PathChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_path = _path->text().toStdString();
}
void MacroActionHttpEdit::ContentTypeChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_contentType = _contentType->text().toStdString();
}
void MacroActionHttpEdit::BodyChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_body = _body->toPlainText().toUtf8().constData();
adjustSize();
updateGeometry();
}
void MacroActionHttpEdit::MethodChanged(int idx)
{
GUARD_LOADING_AND_LOCK();
_entryData->_method = static_cast<MacroActionHttp::Method>(
_methods->itemData(idx).toInt());
SetWidgetVisibility();
}
void MacroActionHttpEdit::TimeoutChanged(const Duration &dur)
{
GUARD_LOADING_AND_LOCK();
_entryData->_timeout = dur;
}
void MacroActionHttpEdit::SetHeadersChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_setHeaders = value;
SetWidgetVisibility();
}
void MacroActionHttpEdit::HeadersChanged(const StringList &headers)
{
GUARD_LOADING_AND_LOCK();
_entryData->_headers = headers;
adjustSize();
updateGeometry();
}
void MacroActionHttpEdit::SetParamsChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_setParams = value;
SetWidgetVisibility();
}
void MacroActionHttpEdit::ParamsChanged(const StringList &params)
{
GUARD_LOADING_AND_LOCK();
_entryData->_params = params;
adjustSize();
updateGeometry();
}
void MacroActionHttpEdit::SetWidgetSignalConnections()
{
QWidget::connect(_url, SIGNAL(editingFinished()), this,
SLOT(URLChanged()));
QWidget::connect(_path, SIGNAL(editingFinished()), this,
SLOT(PathChanged()));
QWidget::connect(_contentType, SIGNAL(editingFinished()), this,
SLOT(ContentTypeChanged()));
QWidget::connect(_body, SIGNAL(textChanged()), this,
SLOT(BodyChanged()));
QWidget::connect(_methods, SIGNAL(currentIndexChanged(int)), this,
SLOT(MethodChanged(int)));
QWidget::connect(_setHeaders, SIGNAL(stateChanged(int)), this,
SLOT(SetHeadersChanged(int)));
QWidget::connect(_headerList,
SIGNAL(StringListChanged(const StringList &)), this,
SLOT(HeadersChanged(const StringList &)));
QWidget::connect(_setParams, SIGNAL(stateChanged(int)), this,
SLOT(SetParamsChanged(int)));
QWidget::connect(_paramList,
SIGNAL(StringListChanged(const StringList &)), this,
SLOT(ParamsChanged(const StringList &)));
QWidget::connect(_timeout, SIGNAL(DurationChanged(const Duration &)),
this, SLOT(TimeoutChanged(const Duration &)));
}
void MacroActionHttpEdit::SetWidgetLayout()
{
const std::unordered_map<std::string, QWidget *> widgets = {
{"{{url}}", _url},
{"{{path}}", _path},
{"{{contentType}}", _contentType},
{"{{method}}", _methods},
{"{{body}}", _body},
{"{{timeout}}", _timeout},
};
auto actionLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.action.http.layout.method"),
actionLayout, widgets);
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.http.layout.contentType"),
_contentTypeLayout, widgets);
_bodyLayout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.action.http.body")));
_bodyLayout->addWidget(_body);
auto timeoutLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.action.http.layout.timeout"),
timeoutLayout, widgets);
_headerListLayout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.action.http.headers")));
_headerListLayout->addWidget(_headerList);
_paramListLayout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.action.http.params")));
_paramListLayout->addWidget(_paramList);
auto layout = new QVBoxLayout;
layout->addLayout(actionLayout);
layout->addWidget(_setHeaders);
layout->addLayout(_headerListLayout);
layout->addWidget(_setParams);
layout->addLayout(_paramListLayout);
layout->addLayout(_contentTypeLayout);
layout->addLayout(_bodyLayout);
layout->addLayout(timeoutLayout);
setLayout(layout);
}
void MacroActionHttpEdit::SetWidgetVisibility()
{
SetLayoutVisible(_headerListLayout, _entryData->_setHeaders);
SetLayoutVisible(_paramListLayout, _entryData->_setParams);
SetLayoutVisible(_contentTypeLayout,
_entryData->_method != MacroActionHttp::Method::GET);
SetLayoutVisible(_bodyLayout,
_entryData->_method != MacroActionHttp::Method::GET);
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@@ -0,0 +1,107 @@
#pragma once
#include "macro-action-edit.hpp"
#include "key-value-list.hpp"
#include "variable-text-edit.hpp"
#include "variable-line-edit.hpp"
#include "duration-control.hpp"
#include <QLineEdit>
#include <QComboBox>
#include <QCheckBox>
namespace advss {
class MacroActionHttp final : public MacroAction {
public:
MacroActionHttp(Macro *m) : MacroAction(m, true) {}
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; };
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
void ResolveVariablesToFixedValues();
enum class Method {
GET = 0,
POST,
PUT,
PATCH,
DELETE,
};
StringVariable _url = "127.0.0.1:8080";
StringVariable _path = "/";
StringVariable _body = obs_module_text("AdvSceneSwitcher.enterText");
StringVariable _contentType = "application/json";
bool _setHeaders = false;
StringList _headers;
bool _setParams = false;
StringList _params;
Method _method = Method::GET;
Duration _timeout = Duration(1.0);
private:
void SetupTempVars();
static bool _registered;
static const std::string id;
};
class MacroActionHttpEdit final : public QWidget {
Q_OBJECT
public:
MacroActionHttpEdit(
QWidget *parent,
std::shared_ptr<MacroActionHttp> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionHttpEdit(
parent,
std::dynamic_pointer_cast<MacroActionHttp>(action));
}
private slots:
void URLChanged();
void PathChanged();
void BodyChanged();
void ContentTypeChanged();
void MethodChanged(int);
void TimeoutChanged(const Duration &seconds);
void SetHeadersChanged(int);
void HeadersChanged(const StringList &);
void SetParamsChanged(int);
void ParamsChanged(const StringList &);
signals:
void HeaderInfoChanged(const QString &);
private:
void SetWidgetSignalConnections();
void SetWidgetLayout();
void SetWidgetVisibility();
VariableLineEdit *_url;
VariableLineEdit *_path;
VariableLineEdit *_contentType;
QHBoxLayout *_contentTypeLayout;
QComboBox *_methods;
VariableTextEdit *_body;
QVBoxLayout *_bodyLayout;
QCheckBox *_setHeaders;
KeyValueListEdit *_headerList;
QVBoxLayout *_headerListLayout;
QCheckBox *_setParams;
KeyValueListEdit *_paramList;
QVBoxLayout *_paramListLayout;
DurationSelection *_timeout;
std::shared_ptr<MacroActionHttp> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@@ -19,9 +19,22 @@ add_library(${PROJECT_NAME} MODULE)
if(OS_WINDOWS)
if(MSVC)
target_compile_options(libremidi PRIVATE /wd4251 /wd4267 /wd4275 /wd4101
/wd4244 /wd4018)
target_compile_options(${PROJECT_NAME} PRIVATE /wd4101 /wd4244 /wd4018)
target_compile_options(
libremidi
PRIVATE /wd4018
/wd4068
/wd4100
/wd4101
/wd4189
/wd4244
/wd4251
/wd4267
/wd4275
/wd4389
/wd4505
/wd4702)
target_compile_options(${PROJECT_NAME} PRIVATE /wd4017 /wd4068 /wd4100
/wd4101 /wd4244)
endif()
else()
target_compile_options(

View File

@@ -10,8 +10,10 @@ if(NOT EXISTS "${CPP_HTTPLIB_DIR}/CMakeLists.txt")
"Twitch support will be disabled!")
return()
endif()
add_subdirectory("${CPP_HTTPLIB_DIR}" "${CPP_HTTPLIB_DIR}/build"
EXCLUDE_FROM_ALL)
if(NOT TARGET httplib)
add_subdirectory("${CPP_HTTPLIB_DIR}" "${CPP_HTTPLIB_DIR}/build"
EXCLUDE_FROM_ALL)
endif()
if(NOT OPENSSL_INCLUDE_DIR OR NOT OPENSSL_LIBRARIES)
find_package(OpenSSL)
@@ -28,6 +30,31 @@ if(NOT ZLIB_FOUND)
return()
endif()
set(DATE_LIB_DIR "${ADVSS_SOURCE_DIR}/deps/date")
if(EXISTS "${DATE_LIB_DIR}/CMakeLists.txt")
set(BUILD_TZ_LIB ON)
if(OS_WINDOWS)
if(CURL_FOUND AND TARGET CURL::libcurl)
get_target_property(CURL_INCLUDE_DIR CURL::libcurl
INTERFACE_INCLUDE_DIRECTORIES)
add_subdirectory("${DATE_LIB_DIR}" "${DATE_LIB_DIR}/build"
EXCLUDE_FROM_ALL)
target_include_directories(date-tz PRIVATE "${CURL_INCLUDE_DIR}")
set(VERIFY_TWITCH_TIMESTAMPS ON)
else()
message(WARNING "CURL not found - not verifying Twitch timestamps")
endif()
else()
add_subdirectory("${DATE_LIB_DIR}" "${DATE_LIB_DIR}/build" EXCLUDE_FROM_ALL)
target_compile_options(date-tz PUBLIC -Wno-error=conversion
-Wno-error=shadow)
set(VERIFY_TWITCH_TIMESTAMPS ON)
endif()
else()
message(WARNING "date lib not found in \"${DATE_LIB_DIR}\"!\n"
"Twitch timestamps will not be checked!")
endif()
# --- End of section ---
add_library(${PROJECT_NAME} MODULE)
@@ -74,6 +101,14 @@ set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
target_include_directories(${PROJECT_NAME} PRIVATE "${CPP_HTTPLIB_DIR}/"
"${OPENSSL_INCLUDE_DIR}")
target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES} ZLIB::ZLIB)
if(DEFINED VERIFY_TWITCH_TIMESTAMPS)
target_compile_definitions(${PROJECT_NAME} PRIVATE VERIFY_TIMESTAMPS=1)
target_link_libraries(${PROJECT_NAME} PRIVATE date::date-tz)
if(OS_WINDOWS)
target_link_libraries(${PROJECT_NAME} PRIVATE CURL::libcurl)
endif()
endif()
install_advss_plugin(${PROJECT_NAME})
if(OS_WINDOWS)
# Couldn't really find a better way to install runtime dependencies for

View File

@@ -4,6 +4,10 @@
#include <log-helper.hpp>
#ifdef VERIFY_TIMESTAMPS
#include "date/tz.h"
#endif
namespace advss {
using websocketpp::lib::placeholders::_1;
@@ -237,15 +241,43 @@ void EventSub::OnOpen(connection_hdl)
static bool isValidTimestamp(const std::string &timestamp)
{
std::tm tm = {};
std::istringstream ss(timestamp);
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S.%fZ");
auto tp = std::chrono::system_clock::from_time_t(std::mktime(&tm));
tp += std::chrono::hours(1); // UTC
std::chrono::system_clock::time_point currentTime =
std::chrono::system_clock::now();
auto diff = currentTime - tp;
return diff <= std::chrono::minutes(10);
#ifdef VERIFY_TIMESTAMPS
// Example input: 2023-07-19T14:56:51.634234626Z
try {
// Discard the nanosecond part
static constexpr size_t dotPos = 19;
std::string trimmed = timestamp.substr(0, dotPos);
auto tzStart = timestamp.find_first_of("Z+-", dotPos);
trimmed = timestamp.substr(0, dotPos);
if (tzStart != std::string::npos) {
trimmed += timestamp.substr(tzStart);
}
std::istringstream in(trimmed);
date::sys_time<std::chrono::seconds> parsedTime;
in >> date::parse("%FT%TZ", parsedTime);
if (in.fail()) {
blog(LOG_WARNING, "failed to parse timestamp %s",
timestamp.c_str());
return false;
}
auto now = date::zoned_time{date::current_zone(),
std::chrono::system_clock::now()}
.get_sys_time();
auto duration = now - parsedTime;
// Clocks might be off by a bit, so allow negative values also
return duration <= std::chrono::minutes(10) &&
duration >= std::chrono::minutes(-1);
} catch (const std::exception &e) {
blog(LOG_WARNING, "%s: %s", __func__, e.what());
return false;
}
#else
// Just assume timestamps are always valid
return true;
#endif
}
bool EventSub::IsValidMessageID(const std::string &id)
@@ -288,7 +320,8 @@ void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
obs_data_get_string(metadata, "message_timestamp");
if (!isValidTimestamp(timestamp)) {
blog(LOG_WARNING,
"Discarding Twitch EventSub with invalid timestamp");
"Discarding Twitch EventSub with invalid timestamp %s",
timestamp.c_str());
return;
}
std::string id = obs_data_get_string(metadata, "message_id");

View File

@@ -826,7 +826,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
_userInfoQueryType(new QComboBox(this)),
_userLogin(new VariableLineEdit(this)),
_userId(new VariableSpinBox(this)),
_pointsReward(new TwitchPointsRewardWidget(this)),
_pointsReward(new TwitchPointsRewardWidget(this, false)),
_rewardVariable(new VariableSelection(this)),
_toggleRewardSelection(new QPushButton())
{

View File

@@ -1450,7 +1450,7 @@ MacroConditionTwitchEdit::MacroConditionTwitchEdit(
_tokens(new TwitchConnectionSelection()),
_tokenWarning(new QLabel()),
_channel(new TwitchChannelSelection(this)),
_pointsReward(new TwitchPointsRewardWidget(this)),
_pointsReward(new TwitchPointsRewardWidget(this, true)),
_streamTitle(new VariableLineEdit(this)),
_regexTitle(new RegexConfigWidget(parent)),
_chatMesageEdit(new ChatMessageEdit(this)),

View File

@@ -1,6 +1,7 @@
#include "points-reward-selection.hpp"
#include "twitch-helpers.hpp"
#include <log-helper.hpp>
#include <obs-module-helper.hpp>
#include <ui-helpers.hpp>
@@ -135,9 +136,10 @@ void TwitchPointsRewardSelection::SelectionChanged(int index)
emit PointsRewardChanged(pointsReward);
}
TwitchPointsRewardWidget::TwitchPointsRewardWidget(QWidget *parent)
TwitchPointsRewardWidget::TwitchPointsRewardWidget(QWidget *parent,
bool allowAny)
: QWidget(parent),
_selection(new TwitchPointsRewardSelection(this)),
_selection(new TwitchPointsRewardSelection(this, allowAny)),
_refreshButton(new QPushButton(this))
{
_refreshButton->setMaximumWidth(22);

View File

@@ -20,7 +20,7 @@ class TwitchPointsRewardSelection : public FilterComboBox {
Q_OBJECT
public:
TwitchPointsRewardSelection(QWidget *parent, bool allowAny = true);
TwitchPointsRewardSelection(QWidget *parent, bool allowAny);
void SetPointsReward(const TwitchPointsReward &pointsReward);
void SetChannel(const TwitchChannel &channel);
@@ -53,7 +53,7 @@ class TwitchPointsRewardWidget : public QWidget {
Q_OBJECT
public:
TwitchPointsRewardWidget(QWidget *parent);
TwitchPointsRewardWidget(QWidget *parent, bool allowAny);
void SetPointsReward(const TwitchPointsReward &pointsReward);
void SetChannel(const TwitchChannel &channel);

View File

@@ -29,17 +29,23 @@ public:
_headers(headers)
{
}
bool operator<(const Args &other) const
bool operator==(const Args &other) const
{
bool ret = true;
ret = ret && _uri < other._uri;
ret = ret && _path < other._path;
ret = ret && _params < other._params;
ret = ret && _data < other._data;
ret = ret && _headers < other._headers;
ret = ret && _uri == other._uri;
ret = ret && _path == other._path;
ret = ret && _params == other._params;
ret = ret && _data == other._data;
ret = ret && _headers == other._headers;
return ret;
}
const std::string &uri() const { return _uri; }
const std::string &path() const { return _path; }
const std::string &data() const { return _data; }
const httplib::Params &params() const { return _params; }
const httplib::Headers &headers() const { return _headers; }
private:
std::string _uri;
std::string _path;
@@ -48,6 +54,37 @@ private:
httplib::Headers _headers;
};
}; // namespace advss
template<> struct std::hash<advss::Args> {
inline std::size_t operator()(const advss::Args &args) const
{
static constexpr auto hash_combine = [](std::size_t &seed,
std::size_t hashValue) {
seed ^= hashValue + 0x9e3779b9 + (seed << 6) +
(seed >> 2);
};
std::size_t seed = 0;
hash_combine(seed, std::hash<std::string>()(args.uri()));
hash_combine(seed, std::hash<std::string>()(args.path()));
hash_combine(seed, std::hash<std::string>()(args.data()));
for (const auto &[key, value] : args.params()) {
hash_combine(seed, std::hash<std::string>()(key));
hash_combine(seed, std::hash<std::string>()(value));
}
for (const auto &[key, value] : args.headers()) {
hash_combine(seed, std::hash<std::string>()(key));
hash_combine(seed, std::hash<std::string>()(value));
}
return seed;
}
};
namespace advss {
struct CacheEntry {
RequestResult result;
std::chrono::system_clock::time_point cacheTime =
@@ -61,14 +98,15 @@ static bool cacheIsTooOld(const CacheEntry &cache)
return diff >= std::chrono::seconds(cacheTimeoutSeconds);
}
static bool cacheIsValid(const std::map<Args, CacheEntry> &cache,
static bool cacheIsValid(const std::unordered_map<Args, CacheEntry> &cache,
const Args &args)
{
auto it = cache.find(args);
return it != cache.end() && !cacheIsTooOld(it->second);
}
static void cleanupCache(std::map<Args, CacheEntry> &cache, std::mutex &mtx)
static void cleanupCache(std::unordered_map<Args, CacheEntry> &cache,
std::mutex &mtx)
{
std::lock_guard<std::mutex> lock(mtx);
cache.clear();
@@ -181,7 +219,7 @@ RequestResult SendGetRequest(const TwitchToken &token, const std::string &uri,
return {};
}
static std::map<Args, CacheEntry> cache;
static std::unordered_map<Args, CacheEntry> cache;
static std::mutex mtx;
[[maybe_unused]] static bool _ = []() {
AddPluginCleanupStep([]() { cleanupCache(cache, mtx); });
@@ -251,7 +289,7 @@ RequestResult SendPostRequest(const TwitchToken &token, const std::string &uri,
return {};
}
static std::map<Args, CacheEntry> cache;
static std::unordered_map<Args, CacheEntry> cache;
static std::mutex mtx;
[[maybe_unused]] static bool _ = []() {
AddPluginCleanupStep([]() { cleanupCache(cache, mtx); });
@@ -322,7 +360,7 @@ RequestResult SendPutRequest(const TwitchToken &token, const std::string &uri,
return {};
}
static std::map<Args, CacheEntry> cache;
static std::unordered_map<Args, CacheEntry> cache;
static std::mutex mtx;
[[maybe_unused]] static bool _ = []() {
AddPluginCleanupStep([]() { cleanupCache(cache, mtx); });
@@ -393,7 +431,7 @@ RequestResult SendPatchRequest(const TwitchToken &token, const std::string &uri,
return {};
}
static std::map<Args, CacheEntry> cache;
static std::unordered_map<Args, CacheEntry> cache;
static std::mutex mtx;
[[maybe_unused]] static bool _ = []() {
AddPluginCleanupStep([]() { cleanupCache(cache, mtx); });

View File

@@ -148,6 +148,39 @@ def script_load(settings):
],
)
# Other signals and procedures, which might be useful
def plugin_stop_handler(data):
obs.script_log(obs.LOG_INFO, "Hello from stop handler!")
get_running_status()
def plugin_start_handler(data):
obs.script_log(obs.LOG_INFO, "Hello from start handler!")
get_running_status()
def get_running_status():
proc_handler = obs.obs_get_proc_handler()
data = obs.calldata_create()
obs.proc_handler_call(proc_handler, "advss_plugin_running", data)
success = obs.calldata_bool(data, "is_running")
obs.script_log(
obs.LOG_INFO, f"The advanced scene switcher is currently running: {success}"
)
obs.calldata_destroy(data)
def interval_reset_handler(data):
obs.script_log(obs.LOG_INFO, "Hello from reset handler!")
signal_handler = obs.obs_get_signal_handler()
obs.signal_handler_connect(
signal_handler, "advss_plugin_stopped", plugin_stop_handler
)
obs.signal_handler_connect(
signal_handler, "advss_plugin_started", plugin_start_handler
)
obs.signal_handler_connect(
signal_handler, "advss_interval_reset", interval_reset_handler
)
def script_unload():
# Deregistering is useful if you plan on reloading the script files