Compare commits

..

99 Commits

Author SHA1 Message Date
WarmUpTill
7ada858046 Add option to match changed content 2023-12-16 19:42:02 +01:00
WarmUpTill
51d121bda2 Add Matches() helper function 2023-12-13 18:31:53 +01:00
WarmUpTill
a7cf5769dd Remove deprecated functionality of "File" condition 2023-12-13 15:23:16 +01:00
Michael Kirsch
63ee36ddfe Enable Area Selection in ScreenshotHelper class 2023-12-13 14:19:17 +01:00
WarmUpTill
43967f5ded Add temp variable support for video condition 2023-12-10 14:26:30 +01:00
WarmUpTill
c043053b2b Remove unused variable 2023-12-10 14:26:30 +01:00
WarmUpTill
220f27bd0a Move plugin dependencies to Aptfile 2023-12-10 13:42:39 +01:00
WarmUpTill
83914d5378 Linux: resolve procps symbols at runtime
This is done to be able to support both libprocps and libproc2.
Either of them or neither might be available on all platforms and this
should not prevent the plugin from being used at all.
2023-12-10 13:42:39 +01:00
WarmUpTill
57e4b1a3f1 Fix warning 2023-12-10 13:42:39 +01:00
WarmUpTill
c85e556924 Add support for "random()" expression
Generates a random number between [0,1)
2023-12-10 13:33:18 +01:00
WarmUpTill
8c0be8011d Enable use of obs-module-helper.hpp in tests 2023-12-10 13:33:18 +01:00
WarmUpTill
7914f75876 Fix macro references not being resolved when importing macros 2023-12-09 09:52:12 +01:00
WarmUpTill
7d1253e14e Use RAII wrappers for OBS data 2023-12-09 09:52:12 +01:00
WarmUpTill
d7c58b3eb3 Remove websocket dependency to switcher-data internals 2023-12-09 09:52:12 +01:00
WarmUpTill
fb664b6f08 Reduce dependencies to switcher-data.hpp implementation details
This is necessary for the switch to libremidi version 4 due to
incompatabilities  with websocketpp, but also might save some headaches
later on.
2023-12-09 09:52:12 +01:00
WarmUpTill
258a6d076e Fix "or not" logic type check being ignored 2023-12-09 09:52:12 +01:00
WarmUpTill
32ab060153 Ease configuration of token grabber port 2023-12-09 09:52:12 +01:00
WarmUpTill
91d3784f9b Don't wait for transition to complete if already on target scene 2023-12-09 09:52:12 +01:00
WarmUpTill
8a5aac3d31 Add option to check the recording duration 2023-12-09 09:52:12 +01:00
WarmUpTill
06c6effb7d Fix duration value not being updated correctly when changing units 2023-12-09 09:52:12 +01:00
WarmUpTill
f3789c05bd Move Duration and DurationSelection into separate files 2023-12-09 09:52:12 +01:00
WarmUpTill
c64e1f5e51 Rework Process condition
* Add option to en-/disable regex matching
* Add tempvar support for process name
2023-12-09 09:52:12 +01:00
WarmUpTill
5d8e24c19a Add temp variable support to Macro condition 2023-12-09 09:52:12 +01:00
WarmUpTill
f756de05c3 Add option to check if macro is paused 2023-12-09 09:52:12 +01:00
WarmUpTill
574fdb496f Add error logs for token grabber server 2023-12-09 09:52:12 +01:00
WarmUpTill
5580da69eb Fix window condition not clearing tempvars of previous condition type 2023-12-09 09:52:12 +01:00
WarmUpTill
35274863c9 Use available layout stretch space for settings input field 2023-12-09 09:52:12 +01:00
Przemek Pawlas
1f31b81f0d Add else actions toggle button 2023-12-09 09:42:10 +01:00
WarmUpTill
cde111ddeb Ignore settings which don't have a description or name
This fixes a crash when attempting to populate the setttings selection
of a Window Capture or Game Capture source
2023-12-07 07:04:50 +01:00
Przemek Pawlas
845c12c01c Add tooltip for run button explaining else (#931)
* Add tooltip for run button explaining else
* Switch run button modifier to Shift
* Toggle button label only when hovering over it
2023-12-06 10:40:14 +01:00
Przemek Pawlas
99a0118d31 Add clipboard text/image copy actions 2023-11-27 21:07:38 +01:00
Przemek Pawlas
bb7ee17362 Prepare Twitch action for introduction of new action types 2023-11-26 17:48:45 +01:00
Przemek Pawlas
70854ef012 Reorder Twitch condition enum value definitions 2023-11-26 17:48:45 +01:00
Przemek Pawlas
053c14056d Add more token options and reorder existing ones 2023-11-26 17:48:45 +01:00
Przemek Pawlas
9397d50764 Refactor twitch-helpers
Added more http helpers, reordered parameters, and reduced code
duplication
2023-11-26 17:48:45 +01:00
Przemek Pawlas
473e24cc42 Use consistent function naming for setting widget visibility 2023-11-26 17:48:45 +01:00
WarmUpTill
d9394605f4 Set tempvar values for raid events 2023-11-26 15:29:54 +01:00
WarmUpTill
d2b9074852 Fix build for Twitch mock 2023-11-26 15:29:54 +01:00
WarmUpTill
b525eb7a0e Add temp variable support to MIDI condition 2023-11-26 13:05:44 +01:00
WarmUpTill
3805aaeeec Add Twitch chat support
* Enable sending chat messages via the Twitch action
* Enable checking for chat messages using the Twitch condition
2023-11-26 00:17:38 +01:00
WarmUpTill
50dc404689 Add temp variable support to date condition 2023-11-20 20:08:09 +01:00
WarmUpTill
975eacbc7e Add temp variable support to timer condition 2023-11-20 20:08:09 +01:00
WarmUpTill
7e56dc35b8 Enable use of variables in find and replace options of variable action 2023-11-20 20:08:09 +01:00
WarmUpTill
b3566d98bf Add condition to check temporary variable values / macro properties 2023-11-20 20:08:09 +01:00
WarmUpTill
15379a8e20 Clean up locale and variable condition 2023-11-20 20:08:09 +01:00
WarmUpTill
1e178f4223 Add temp variable support to scene condition 2023-11-20 20:08:09 +01:00
WarmUpTill
c16a7b93ea Add temp variable support to window condition 2023-11-20 20:08:09 +01:00
WarmUpTill
f89bb1a750 Add temp variable support for audio condition 2023-11-20 20:08:09 +01:00
WarmUpTill
79961aabe4 Use SourceSetting helpers to enable modification of single setting 2023-11-20 20:08:09 +01:00
WarmUpTill
4686fc2b01 Add SourceSetting helpers
They enable easy modification of single setting value of sources
compared to working with the raw json string
2023-11-20 20:08:09 +01:00
WarmUpTill
569b915c07 Set temporary variables in Twitch condition 2023-11-20 20:08:09 +01:00
WarmUpTill
324d3fe7a5 Fix content content_classification_labels not being set 2023-11-20 20:08:09 +01:00
WarmUpTill
ae8d0bf5cb Add support for setting variable to value of temporary variable 2023-11-20 20:08:09 +01:00
WarmUpTill
17efa82057 Add temporary variables
Enables easier use of values returned by macro segments.
For example, this enables the user to extract the user name of a new
Twitch follower and change a text sources settings accordingly
2023-11-20 20:08:09 +01:00
WarmUpTill
c17523a476 Enable extraction of array values from channel info 2023-11-20 20:08:09 +01:00
WarmUpTill
33635d6991 Add action to enable extraction of values from json string 2023-11-20 20:08:09 +01:00
WarmUpTill
d290dbe86b Add nlohmann json dependency 2023-11-20 20:08:09 +01:00
WarmUpTill
c1f5c49eda Run PostLoad() for "else actions" as well 2023-11-20 20:08:09 +01:00
WarmUpTill
c3f4caa6f3 Add AddPostLoadStep()
Enables registering additional functions, which are to be executed after
the loading steps are complete
2023-11-20 20:08:09 +01:00
WarmUpTill
d4c7b0e455 Fix crash while running macro actions
Users could invalidate the actions vector while the actions were
executed resulting in a crash.
2023-11-20 20:08:09 +01:00
WarmUpTill
b0cb52fe90 Fix warning 2023-11-20 20:08:09 +01:00
WarmUpTill
386f831cd4 Fix typos 2023-11-20 20:08:09 +01:00
Przemek Pawlas
449daa80cc Add media seek to percentage action + cleanup 2023-11-17 20:11:00 +01:00
Przemek Pawlas
3003547b83 Rework Twitch condition
* Fix channel changes in condition not resetting subscription ID
* Simplified Twitch condition logic to match the subscription ID rather
  than broadcaster ID and some other data - worked well for me, as I
  mentioned not sure why it wasn't done this way in the 1st place (but
  if there's a reason let me know)
* Further made ordering more consistent so that Twitch macros/conditions
  are easier to read (more or less samethematical order in hpp and cpp)
* Removed some unused code
* Added some param names to some header files to functions
2023-11-08 19:59:59 +01:00
Przemek Pawlas
1664eb672f Verify if channel given channel name is valid 2023-11-08 19:59:59 +01:00
Przemek Pawlas
0939dcacb6 Cleanup 2023-11-08 19:59:59 +01:00
Przemek Pawlas
54b81d1a42 Add AnyOptionIsEnabled() 2023-11-08 19:59:59 +01:00
Przemek Pawlas
626606100f Add helper classes to handle Twitch channel point rewards 2023-11-08 19:59:59 +01:00
Przemek Pawlas
53a0b76e8c Add empty() 2023-11-08 19:59:59 +01:00
WarmUpTill
e16ac9400a Add option to set variable value to length of string
* Also use SringVariable for _strValue input field to enable getting the
  length of variable values
2023-10-25 15:06:11 +02:00
WarmUpTill
94060c236b Add option to set ResizingPlainTextEdit parameters 2023-10-25 15:06:11 +02:00
Przemek Pawlas
890792c270 Add more Twitch condition types
Also simplifies some code and make it more consistent
2023-10-25 14:47:34 +02:00
WarmUpTill
7bc1fc3d0d Fix crash if previous condition / action was invalid 2023-10-25 14:47:34 +02:00
WarmUpTill
9ea90ed61b Add Twitch condition
It supports:
* Checking if a channel is live (polling)
* Checking if a channel went live and it's stream type (event)
* Checking if a channel weng offline (event)
* Checking the stream title of a channel (polling)
* Checking the stream category of a channel (polling)
* Checking if the channel information was changed (event)
* Checking for new followers (event)
* Checking for subsciptions (new / end / gift / resub message) (event)
* Checking for cheers (event)
2023-10-14 21:22:53 +02:00
WarmUpTill
ce40b80d90 Clean up token selection
* Adjust locale
* Hide token and name fields if no account is connected
* Enable all token options by default
* Validate token every hour as required by Twitch
* Retrigger event subscriptions on token change

token
2023-10-14 21:22:53 +02:00
WarmUpTill
53fcd94c5a Add Twitch EventSub helpers
EventSub subscriptions let your application listen for events that
happen on Twitch.
2023-10-14 21:22:53 +02:00
WarmUpTill
ecb6247959 Combine manual category search button and combobox into a single widget
To avoid code duplication for Twitch condition and action
2023-10-14 21:22:53 +02:00
WarmUpTill
3b4cb6934d Prevent crashes when downgrading to older version of Twitch action 2023-10-14 21:22:53 +02:00
WarmUpTill
babe93a6ce Clean up Twitch action
* Allow filtering in action selection by typing
* Add spacing between action type definitions to allow reordering in
  future
2023-10-14 21:22:53 +02:00
WarmUpTill
1e9173699c Add option to start a channel raid 2023-10-14 21:22:53 +02:00
WarmUpTill
37ba7aa2ca Only change text of segment value preview if value changed
This is done to prevent the selection being lost whenever the text filed
was updated.
2023-10-14 21:22:53 +02:00
WarmUpTill
a83d2d9910 Add channel selection 2023-10-14 21:22:53 +02:00
WarmUpTill
0449064f5d Enable empty name warning to be hidden
Used for the twitch token selection for which the whole name selection
will be hidden if no token is connected.
2023-10-14 21:22:53 +02:00
WarmUpTill
6cf1073a3c Add option to use cached results
The cache will remain valid for 10s before it is refreshed with a "real"
Get / Post / Patch call.
2023-10-14 21:22:53 +02:00
WarmUpTill
9ac9171206 Fix windows being created when constructing ProgressDialog 2023-10-14 21:22:53 +02:00
WarmUpTill
25ce8cd2e4 Also ex-/import variables when ex-/importing macros
Manually creating necessary variables and reconfiguring the macros to
use them can be unintuitive so instead just create the missing variables
included in the export.
2023-10-11 21:42:05 +02:00
WarmUpTill
e50522ab89 Compress export / import macro text 2023-10-11 21:42:05 +02:00
WarmUpTill
4801502f32 Add option to open properties and filter windows of sources 2023-10-05 18:54:22 +02:00
Przemek Pawlas
e13e4b5ddd Add max length validation support for TextEdit 2023-10-05 18:53:46 +02:00
Michael Kirsch
de8da553f1 Implement source item transform changed condition (#885) 2023-10-03 19:55:45 +02:00
Przemek Pawlas
6558792e73 Add Twitch send chat announcement and emote-only mode actions (#890) 2023-10-03 14:09:29 +02:00
WarmUpTill
ea5e92017e Add "all" option in filter selection 2023-10-03 12:34:11 +02:00
Przemek Pawlas
46199efb58 Add tooltips for variable text inputs (#888) 2023-10-03 11:42:41 +02:00
WarmUpTill
8a6eae6c97 CI: Fix Windows build script appending new lines to CMakeLists.txt 2023-10-03 00:09:29 +02:00
WarmUpTill
67eef8a004 CI: Fix MacOS builds
This is just a temporary fix.
The real solution would be to switch to the new OBS plugin template
version and no longer rely on the packages app
2023-10-03 00:09:29 +02:00
WarmUpTill
b5341e5cde Hide option to set master volume (deprecated) 2023-09-21 23:52:14 +02:00
WarmUpTill
ac6140b17a Fix warnings 2023-09-21 23:52:14 +02:00
WarmUpTill
b1489d98f0 Update websocketpp to 0.8.2 2023-09-17 19:59:30 +02:00
WarmUpTill
3b7119ec58 Update Asio to 1.28.0 2023-09-17 19:59:30 +02:00
151 changed files with 10826 additions and 1612 deletions

View File

@@ -7,3 +7,8 @@ package 'ninja-build', bin: 'ninja'
package 'pkg-config'
package 'clang'
package 'clang-format-13'
package 'libxtst-dev'
package 'libxss-dev'
package 'libopencv-dev'
package 'libtesseract-dev'
package 'libprocps-dev'

View File

@@ -71,7 +71,7 @@ function Build {
(Get-Content -Path ${ProjectRoot}/CMakeLists.txt -Raw) `
-replace "project\((.*) VERSION (.*)\)", "project(${ProductName} VERSION ${ProductVersion})" `
| Out-File -Path ${ProjectRoot}/CMakeLists.txt
| Out-File -Path ${ProjectRoot}/CMakeLists.txt -NoNewline
Setup-Obs

View File

@@ -24,7 +24,7 @@ if (( ! ${+commands[packagesbuild]} )) {
pushd
mkcd ${project_root:h}/obs-build-dependencies
local packages_url='http://s.sudre.free.fr/Software/files/Packages.dmg'
local packages_url='https://web.archive.org/web/20230727054218/http://s.sudre.free.fr/Software/files/Packages.dmg'
local packages_hash='6afdd25386295974dad8f078b8f1e41cabebd08e72d970bf92f707c7e48b16c9'
if [[ ! -f Packages.dmg ]] {

View File

@@ -34,13 +34,6 @@ if (( ! (${skips[(Ie)all]} + ${skips[(Ie)deps]}) )) {
log_info 'Installing obs plugin dependencies...'
sudo apt-get install -y \
libxtst-dev \
libxss-dev \
libopencv-dev \
libtesseract-dev \
libprocps-dev
local -a _qt_packages=()
if (( QT_VERSION == 5 )) {

3
.gitmodules vendored
View File

@@ -22,3 +22,6 @@
[submodule "deps/cpp-httplib"]
path = deps/cpp-httplib
url = https://github.com/yhirose/cpp-httplib.git
[submodule "deps/json"]
path = deps/json
url = https://github.com/nlohmann/json.git

View File

@@ -93,6 +93,8 @@ target_sources(
${LIB_NAME}
PRIVATE src/macro-core/macro-action-audio.cpp
src/macro-core/macro-action-audio.hpp
src/macro-core/macro-action-clipboard.cpp
src/macro-core/macro-action-clipboard.hpp
src/macro-core/macro-action-edit.cpp
src/macro-core/macro-action-edit.hpp
src/macro-core/macro-action-file.cpp
@@ -213,6 +215,8 @@ target_sources(
src/macro-core/macro-condition-streaming.hpp
src/macro-core/macro-condition-studio-mode.cpp
src/macro-core/macro-condition-studio-mode.hpp
src/macro-core/macro-condition-tempvar.cpp
src/macro-core/macro-condition-tempvar.hpp
src/macro-core/macro-condition-timer.cpp
src/macro-core/macro-condition-timer.hpp
src/macro-core/macro-condition-transition.cpp
@@ -252,6 +256,8 @@ target_sources(
src/utils/connection-manager.hpp
src/utils/curl-helper.cpp
src/utils/curl-helper.hpp
src/utils/duration.cpp
src/utils/duration.hpp
src/utils/duration-control.cpp
src/utils/duration-control.hpp
src/utils/export-symbol-helper.hpp
@@ -285,6 +291,8 @@ target_sources(
src/utils/obs-module-helper.hpp
src/utils/osc-helpers.cpp
src/utils/osc-helpers.hpp
src/utils/plugin-state-helper.cpp
src/utils/plugin-state-helper.hpp
src/utils/priority-helper.cpp
src/utils/priority-helper.hpp
src/utils/process-config.cpp
@@ -307,6 +315,8 @@ target_sources(
src/utils/slider-spinbox.hpp
src/utils/source-selection.cpp
src/utils/source-selection.hpp
src/utils/source-setting.cpp
src/utils/source-setting.hpp
src/utils/string-list.cpp
src/utils/string-list.hpp
src/utils/striped-frame.cpp
@@ -315,6 +325,8 @@ target_sources(
src/utils/switch-button.hpp
src/utils/sync-helper.cpp
src/utils/sync-helper.hpp
src/utils/temp-variable.cpp
src/utils/temp-variable.hpp
src/utils/transition-selection.cpp
src/utils/transition-selection.hpp
src/utils/utility.cpp
@@ -378,6 +390,12 @@ target_include_directories(
"${CMAKE_CURRENT_SOURCE_DIR}/deps/obs-websocket/lib"
"${CMAKE_CURRENT_SOURCE_DIR}/deps/exprtk")
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/deps/json/CMakeLists.txt")
add_subdirectory(deps/json)
else()
find_package(nlohmann_json REQUIRED)
endif()
target_link_libraries(${LIB_NAME} PUBLIC nlohmann_json::nlohmann_json)
target_compile_definitions(${LIB_NAME} PRIVATE ADVSS_EXPORT_SYMBOLS=1)
# --- End of section ---
@@ -446,27 +464,24 @@ else()
find_path(PROCPS_INCLUDE_DIR NAMES proc/procps.h)
find_path(PROCPS2_INCLUDE_DIR NAMES libproc2/pids.h)
find_library(PROCPS_LIBRARY NAMES procps)
find_library(PROCPS2_LIBRARY NAMES proc2)
if(PROCPS_INCLUDE_DIR AND PROCPS_LIBRARY)
if(PROCPS_INCLUDE_DIR)
message(STATUS "${PROJECT_NAME} using procps")
set(PROC_INCLUDE_DIR "${PROCPS_INCLUDE_DIR}")
set(PROC_LIBRARY "${PROCPS_LIBRARY}")
target_compile_definitions(${LIB_NAME} PRIVATE USE_PROCPS)
elseif(PROCPS2_INCLUDE_DIR AND PROCPS2_LIBRARY)
target_compile_definitions(${LIB_NAME} PRIVATE PROCPS_AVAILABLE)
set(PROCESS_CONDITION_SUPPORTED 1)
endif()
if(PROCPS2_INCLUDE_DIR)
message(STATUS "${PROJECT_NAME} using libproc2")
set(PROC_INCLUDE_DIR "${PROCPS2_INCLUDE_DIR}")
set(PROC_LIBRARY "${PROCPS2_LIBRARY}")
else()
message(
FATAL_ERROR
"found neither procps nor libproc2!
please either set ...
PROCPS_INCLUDE_DIR and PROCPS_LIBRARY
... or ...
PROCPS2_INCLUDE_DIR and PROCPS2_LIBRARY")
target_compile_definitions(${LIB_NAME} PRIVATE PROCPS2_AVAILABLE)
set(PROCESS_CONDITION_SUPPORTED 1)
endif()
if(NOT DEFINED PROCESS_CONDITION_SUPPORTED)
message(
WARNING
"found neither procps nor libproc2! Process condition will not be functional!"
)
endif()
target_link_libraries(${LIB_NAME} PRIVATE "${PROC_LIBRARY}")
target_include_directories(${LIB_NAME} PRIVATE "${PROC_INCLUDE_DIR}")
find_package(CURL)

View File

@@ -152,9 +152,6 @@ AdvSceneSwitcher.condition.file.type.contentChange="Inhalt geändert"
AdvSceneSwitcher.condition.file.type.dateChange="Änderungsdatum geändert"
AdvSceneSwitcher.condition.file.remote="Entfernte Datei"
AdvSceneSwitcher.condition.file.local="Lokale Datei"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Medien"
AdvSceneSwitcher.condition.media.source="Quelle"
AdvSceneSwitcher.condition.media.anyOnScene="Beliebige Medienquelle in"
@@ -221,9 +218,8 @@ AdvSceneSwitcher.condition.record="Aufnahme"
AdvSceneSwitcher.condition.record.state.start="Aufnahme läuft"
AdvSceneSwitcher.condition.record.state.pause="Aufnahme pausiert"
AdvSceneSwitcher.condition.record.state.stop="Aufnahme gestoppt"
AdvSceneSwitcher.condition.record.entry="{{recordState}}"
AdvSceneSwitcher.condition.process="Prozess"
AdvSceneSwitcher.condition.process.entry="{{processes}} läuft {{focused}} und ist fokusiert"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}läuft{{focused}}und ist fokusiert"
AdvSceneSwitcher.condition.process.entry.focus="Aktueller Vordergrundprozess: {{focusProcess}}"
AdvSceneSwitcher.condition.idle="Leerlauf"
AdvSceneSwitcher.condition.idle.entry="Keine Tastatur- oder Mauseingaben für {{duration}}"
@@ -325,7 +321,9 @@ AdvSceneSwitcher.condition.date.entry.nextMatchDate="Nächster Treffer bei: %1"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}} Bei Wiederholung ausgewähltes Datum auf Wiederholungsdatum aktualisieren"
AdvSceneSwitcher.condition.sceneTransform="Szenenelement transformieren"
AdvSceneSwitcher.condition.sceneTransform.getTransform="Transformation erhalten"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="Auf{{scenes}}{{sources}}passt zur Transformation"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="Auf{{scenes}}{{sources}}{{types}}"
AdvSceneSwitcher.condition.sceneTransform.entry.type.matches="entspricht Transformation"
AdvSceneSwitcher.condition.sceneTransform.entry.type.changed="hat sich geändert"
AdvSceneSwitcher.condition.sceneTransform.entry.line2="{{settings}}"
AdvSceneSwitcher.condition.sceneTransform.entry.line3="{{regex}} {{getSettings}}"
AdvSceneSwitcher.condition.transition="Übergang"
@@ -389,7 +387,6 @@ AdvSceneSwitcher.condition.variable.type.valueChanged="Wert verändert"
AdvSceneSwitcher.condition.variable.type.equalsVariable="entspricht Variable"
AdvSceneSwitcher.condition.variable.type.lessThanVariable="ist kleiner als die Variable"
AdvSceneSwitcher.condition.variable.type.greaterThanVariable="ist größer als die Variable"
AdvSceneSwitcher.condition.variable.entry="{{variables}}{{conditions}}{{strValue}}{{numValue}}{{variables2}}"
; Macro Actions
AdvSceneSwitcher.action.scene="Szene wechseln"
@@ -468,8 +465,7 @@ AdvSceneSwitcher.action.media.type.stop="Stoppen"
AdvSceneSwitcher.action.media.type.restart="Neu starten"
AdvSceneSwitcher.action.media.type.next="Nächste"
AdvSceneSwitcher.action.media.type.previous="Vorherige"
AdvSceneSwitcher.action.media.type.seek="Suche nach"
AdvSceneSwitcher.action.media.entry="{{actions}}{{duration}}{{mediaSources}}"
AdvSceneSwitcher.action.media.type.seek.duration="Suche nach"
AdvSceneSwitcher.action.macro="Makro"
AdvSceneSwitcher.action.macro.type.pause="Pausieren"
AdvSceneSwitcher.action.macro.type.unpause="Nicht mehr pausieren"
@@ -575,7 +571,7 @@ AdvSceneSwitcher.action.variable.invalidSelection="Ungültige Auswahl!"
AdvSceneSwitcher.action.variable.actionNoVariableSupport="Das Abrufen von Variablenwerten aus %1 Aktionen wird nicht unterstützt!"
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="Das Abrufen von Variablenwerten aus %1 Bedingungen wird nicht unterstützt!"
AdvSceneSwitcher.action.variable.currentSegmentValue="Aktueller Wert:"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}{{tempVars}}"
; Transition Tab
@@ -713,7 +709,7 @@ AdvSceneSwitcher.sceneSequenceTab.loadFail="Laden der Einstellungen ist fehlgesc
AdvSceneSwitcher.sceneSequenceTab.loadSuccess="Laden der Einstellungen war erfolgreich!"
AdvSceneSwitcher.sceneSequenceTab.fileType="Text Dateien (*.txt)"
AdvSceneSwitcher.sceneSequenceTab.interruptible="kann unterbrochen werden"
AdvSceneSwitcher.sceneSequenceTab.interruptibleHint="Andere Szenenwechselmethoden können diese Sequenz unterbrechen"
AdvSceneSwitcher.sceneSequenceTab.interruptibleHint="Andere Szenenwechselmethoden können diese Sequenz unterbrechen"
AdvSceneSwitcher.sceneSequenceTab.entry="Wenn {{startScenes}} aktiv ist wechsle zu {{scenes}} nach {{delay}} mit {{transitions}} {{interruptible}}"
AdvSceneSwitcher.sceneSequenceTab.extendEdit="Szenenfolge erweitern"
AdvSceneSwitcher.sceneSequenceTab.extendEntry="Nach {{delay}} wechsle zu {{scenes}} mit {{transitions}}"

View File

@@ -79,6 +79,7 @@ AdvSceneSwitcher.macroTab.edit.action="Action type:"
AdvSceneSwitcher.macroTab.add="Add new macro"
AdvSceneSwitcher.macroTab.name="Name:"
AdvSceneSwitcher.macroTab.run="Run macro"
AdvSceneSwitcher.macroTab.run.tooltip="Run all macro actions regardless of condition.\nIf else actions are available, hold Shift and click the button to execute them instead."
AdvSceneSwitcher.macroTab.runElse="Run macro (else)"
AdvSceneSwitcher.macroTab.runFail="Running \"%1\" failed!\nEither one of the actions failed or the macro is running already."
AdvSceneSwitcher.macroTab.runInParallel="Run macro in parallel to other macros"
@@ -125,6 +126,8 @@ AdvSceneSwitcher.macroTab.currentDockButtonText.unpause="Unpause button text:"
AdvSceneSwitcher.macroTab.currentDockStatusText.true="Conditions true text:"
AdvSceneSwitcher.macroTab.currentDockStatusText.false="Conditions false text:"
AdvSceneSwitcher.macroTab.currentDockHighlightIfExecuted="Highlight dock if macro actions were recently executed"
AdvSceneSwitcher.macroTab.toggleElseActions.hide.tooltip="Hide else actions"
AdvSceneSwitcher.macroTab.toggleElseActions.show.tooltip="Show else actions"
AdvSceneSwitcher.macroDock.pause="Pause"
AdvSceneSwitcher.macroDock.unpause="Unpause"
@@ -192,14 +195,17 @@ AdvSceneSwitcher.condition.window.entry.text="{{checkText}}Window contains text{
AdvSceneSwitcher.condition.window.entry.text.note="This option might not work on every text being displayed in a window.\nIf that is the case, you can consider using the Video conditions OCR check instead."
AdvSceneSwitcher.condition.window.entry.currentFocus="Current focus window:{{focusWindow}}"
AdvSceneSwitcher.condition.file="File"
AdvSceneSwitcher.condition.file.type.match="matches"
AdvSceneSwitcher.condition.file.type.contentChange="content changed"
AdvSceneSwitcher.condition.file.type.dateChange="modification date changed"
AdvSceneSwitcher.condition.file.type.match="Matches"
AdvSceneSwitcher.condition.file.type.contentChange="Content changed"
AdvSceneSwitcher.condition.file.type.dateChange="Modification date changed"
AdvSceneSwitcher.condition.file.type.changesMatch="Changed lines match"
AdvSceneSwitcher.condition.file.remote="Remote file"
AdvSceneSwitcher.condition.file.local="Local file"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.changeMatchType.any="Any lines"
AdvSceneSwitcher.condition.file.changeMatchType.added="Added lines"
AdvSceneSwitcher.condition.file.changeMatchType.removed="Removed lines"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{changeMatchType}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Media"
AdvSceneSwitcher.condition.media.source="Source"
AdvSceneSwitcher.condition.media.anyOnScene="Any media source on"
@@ -295,9 +301,10 @@ AdvSceneSwitcher.condition.record="Recording"
AdvSceneSwitcher.condition.record.state.start="Recording running"
AdvSceneSwitcher.condition.record.state.pause="Recording paused"
AdvSceneSwitcher.condition.record.state.stop="Recording stopped"
AdvSceneSwitcher.condition.record.entry="{{recordState}}"
AdvSceneSwitcher.condition.record.state.duration="Recording duration is longer than"
AdvSceneSwitcher.condition.record.entry="{{condition}}{{duration}}"
AdvSceneSwitcher.condition.process="Process"
AdvSceneSwitcher.condition.process.entry="{{processes}}is running{{focused}}and is focused"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}is running{{focused}}and is focused"
AdvSceneSwitcher.condition.process.entry.focus="Current foreground process:{{focusProcess}}"
AdvSceneSwitcher.condition.idle="Idle"
AdvSceneSwitcher.condition.idle.entry="No keyboard or mouse inputs for{{duration}}"
@@ -326,6 +333,7 @@ AdvSceneSwitcher.condition.macro.type.state="Macro condition state"
AdvSceneSwitcher.condition.macro.type.multiState="Multiple macro condition state"
AdvSceneSwitcher.condition.macro.type.actionDisabled="Macro action disabled"
AdvSceneSwitcher.condition.macro.type.actionEnabled="Macro action enabled"
AdvSceneSwitcher.condition.macro.type.paused="Macro is paused"
AdvSceneSwitcher.condition.macro.type.selection="{{types}}"
AdvSceneSwitcher.condition.macro.count.type.below="Less than"
AdvSceneSwitcher.condition.macro.count.type.above="More than"
@@ -341,6 +349,7 @@ AdvSceneSwitcher.condition.macro.count.entry.line1="{{macros}}was executed{{cond
AdvSceneSwitcher.condition.macro.count.entry.line2="Current count:{{currentCount}}{{resetCount}}"
AdvSceneSwitcher.condition.macro.actionState.disabled.entry="Action{{actionIndex}}of{{macros}}is disabled"
AdvSceneSwitcher.condition.macro.actionState.enabled.entry="Action{{actionIndex}}of{{macros}}is enabled"
AdvSceneSwitcher.condition.macro.paused.entry="Macro{{macros}}is paused"
AdvSceneSwitcher.condition.source="Source"
AdvSceneSwitcher.condition.source.type.active="Is active"
AdvSceneSwitcher.condition.source.type.showing="Is showing"
@@ -407,7 +416,9 @@ AdvSceneSwitcher.condition.date.entry.nextMatchDate="Next match at: %1"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}}On repeat update selected date to repeat date"
AdvSceneSwitcher.condition.sceneTransform="Scene item transform"
AdvSceneSwitcher.condition.sceneTransform.getTransform="Get transform"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="On{{scenes}}{{sources}}matches transform"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="On{{scenes}}{{sources}}{{types}}"
AdvSceneSwitcher.condition.sceneTransform.entry.type.matches="matches transform"
AdvSceneSwitcher.condition.sceneTransform.entry.type.changed="has changed"
AdvSceneSwitcher.condition.sceneTransform.entry.line2="{{settings}}"
AdvSceneSwitcher.condition.sceneTransform.entry.line3="{{regex}}{{getSettings}}"
AdvSceneSwitcher.condition.transition="Transition"
@@ -461,6 +472,7 @@ AdvSceneSwitcher.condition.websocket.type.event="Scene Switcher Event"
AdvSceneSwitcher.condition.websocket.useRegex="Use regular expressions"
AdvSceneSwitcher.condition.websocket.entry.request="{{type}}was received:"
AdvSceneSwitcher.condition.websocket.entry.event="{{type}}was received from{{connection}}:"
AdvSceneSwitcher.condition.temporaryVariable="Macro property"
AdvSceneSwitcher.condition.variable="Variable"
AdvSceneSwitcher.condition.variable.type.compare="equals"
AdvSceneSwitcher.condition.variable.type.empty="is empty"
@@ -471,7 +483,7 @@ AdvSceneSwitcher.condition.variable.type.valueChanged="value changed"
AdvSceneSwitcher.condition.variable.type.equalsVariable="equals variable"
AdvSceneSwitcher.condition.variable.type.lessThanVariable="is less than variable"
AdvSceneSwitcher.condition.variable.type.greaterThanVariable="is greater than variable"
AdvSceneSwitcher.condition.variable.entry="{{variables}}{{conditions}}{{strValue}}{{numValue}}{{variables2}}"
AdvSceneSwitcher.condition.variable.entry="{{variables}}{{conditions}}{{strValue}}{{numValue}}{{variables2}}{{regex}}"
AdvSceneSwitcher.condition.run="Run"
AdvSceneSwitcher.condition.run.entry="Process exits before timeout of{{timeout}}seconds"
AdvSceneSwitcher.condition.run.entry.exit="{{checkExitCode}}Check for exit code{{exitCode}}"
@@ -488,6 +500,62 @@ AdvSceneSwitcher.condition.slideshow.condition.slideIndex="Current slide number
AdvSceneSwitcher.condition.slideshow.condition.slidePath="Current slide path is"
AdvSceneSwitcher.condition.slideshow.updateIntervalTooltip="Information about the slide show status will only be updated based on the configured time between slides"
AdvSceneSwitcher.condition.slideshow.entry="{{sources}}{{conditions}}{{index}}{{path}}"
AdvSceneSwitcher.condition.twitch="Twitch"
AdvSceneSwitcher.condition.twitch.type.event.channel.stream.online="Stream went online"
AdvSceneSwitcher.condition.twitch.type.event.channel.stream.offline="Stream went offline"
AdvSceneSwitcher.condition.twitch.type.event.channel.info.update="Channel information updated"
AdvSceneSwitcher.condition.twitch.type.event.channel.follow="Follow received"
AdvSceneSwitcher.condition.twitch.type.event.channel.subscription.start="Subscription (no resubscriptions included) started"
AdvSceneSwitcher.condition.twitch.type.event.channel.subscription.end="Subscription ended"
AdvSceneSwitcher.condition.twitch.type.event.channel.subscription.gift="Subscription(s) gifted"
AdvSceneSwitcher.condition.twitch.type.event.channel.subscription.message="Resubscription chat message received"
AdvSceneSwitcher.condition.twitch.type.event.channel.cheer="Cheer chat message received"
AdvSceneSwitcher.condition.twitch.type.event.channel.raid.outbound="Raided someone's channel"
AdvSceneSwitcher.condition.twitch.type.event.channel.raid.inbound="Raid to your channel occured"
AdvSceneSwitcher.condition.twitch.type.event.channel.shoutout.outbound="Shoutout to someone's channel given"
AdvSceneSwitcher.condition.twitch.type.event.channel.shoutout.inbound="Shoutout to your channel received"
AdvSceneSwitcher.condition.twitch.type.event.channel.poll.start="Poll started"
AdvSceneSwitcher.condition.twitch.type.event.channel.poll.progress="Poll votes changed (progress)"
AdvSceneSwitcher.condition.twitch.type.event.channel.poll.end="Poll ended"
AdvSceneSwitcher.condition.twitch.type.event.channel.prediction.start="Prediction started"
AdvSceneSwitcher.condition.twitch.type.event.channel.prediction.progress="Prediction bets changed (progress)"
AdvSceneSwitcher.condition.twitch.type.event.channel.prediction.lock="Prediction bets locked"
AdvSceneSwitcher.condition.twitch.type.event.channel.prediction.end="Prediction ended"
AdvSceneSwitcher.condition.twitch.type.event.channel.goal.start="Goal started"
AdvSceneSwitcher.condition.twitch.type.event.channel.goal.progress="Goal follow stats changed (progress)"
AdvSceneSwitcher.condition.twitch.type.event.channel.goal.end="Goal ended"
AdvSceneSwitcher.condition.twitch.type.event.channel.hypeTrain.start="Hype Train started"
AdvSceneSwitcher.condition.twitch.type.event.channel.hypeTrain.progress="Hype Train stats changed (progress)"
AdvSceneSwitcher.condition.twitch.type.event.channel.hypeTrain.end="Hype Train ended"
AdvSceneSwitcher.condition.twitch.type.event.channel.charityCampaign.start="Charity campaign started"
AdvSceneSwitcher.condition.twitch.type.event.channel.charityCampaign.progress="Charity campaign stats changed (progress)"
AdvSceneSwitcher.condition.twitch.type.event.channel.charityCampaign.donation="Charity campaign donation occured"
AdvSceneSwitcher.condition.twitch.type.event.channel.charityCampaign.end="Charity campaign ended"
AdvSceneSwitcher.condition.twitch.type.event.channel.shieldMode.start="Shield Mode enabled"
AdvSceneSwitcher.condition.twitch.type.event.channel.shieldMode.end="Shield Mode disabled"
AdvSceneSwitcher.condition.twitch.type.event.channel.points.reward.addition="Points reward added"
AdvSceneSwitcher.condition.twitch.type.event.channel.points.reward.update="Points reward updated"
AdvSceneSwitcher.condition.twitch.type.event.channel.points.reward.deletion="Points reward deleted"
AdvSceneSwitcher.condition.twitch.type.event.channel.points.reward.redemption="Points reward redeemed"
AdvSceneSwitcher.condition.twitch.type.event.channel.points.reward.redemption.update="Points reward redemption updated"
AdvSceneSwitcher.condition.twitch.type.event.channel.user.ban="User banned from channel"
AdvSceneSwitcher.condition.twitch.type.event.channel.user.unban="User unbanned from channel"
AdvSceneSwitcher.condition.twitch.type.event.channel.user.moderator.addition="User added as channel moderator"
AdvSceneSwitcher.condition.twitch.type.event.channel.user.moderator.deletion="User deleted from channel moderation"
AdvSceneSwitcher.condition.twitch.type.event.channel.stream.online.live="Stream went live"
AdvSceneSwitcher.condition.twitch.type.event.channel.stream.online.playlist="Stream started playlist"
AdvSceneSwitcher.condition.twitch.type.event.channel.stream.online.watchParty="Stream started watchparty"
AdvSceneSwitcher.condition.twitch.type.event.channel.stream.online.premiere="Stream started premiere"
AdvSceneSwitcher.condition.twitch.type.event.channel.stream.online.rerun="Stream started rerun"
AdvSceneSwitcher.condition.twitch.type.polling.channel.live="Stream is currently live"
AdvSceneSwitcher.condition.twitch.type.polling.channel.title="Current title matches"
AdvSceneSwitcher.condition.twitch.type.polling.channel.category="Current category is"
AdvSceneSwitcher.condition.twitch.type.chat.message="Chat message received"
AdvSceneSwitcher.condition.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!"
AdvSceneSwitcher.condition.twitch.entry="Channel{{channel}}{{conditions}}{{pointsReward}}{{streamTitle}}{{regex}}{{category}}"
AdvSceneSwitcher.condition.twitch.entry.account="Check using account{{account}}"
AdvSceneSwitcher.condition.twitch.tokenPermissionsInsufficient="Permissions of selected token are insufficient to perform selected action!"
AdvSceneSwitcher.condition.twitch.title.title="Enter title"
; Macro Actions
AdvSceneSwitcher.action.scene="Switch scene"
@@ -558,7 +626,11 @@ AdvSceneSwitcher.action.filter.type.disable="Disable"
AdvSceneSwitcher.action.filter.type.toggle="Toggle"
AdvSceneSwitcher.action.filter.type.settings="Set settings"
AdvSceneSwitcher.action.filter.entry="On{{sources}}{{actions}}{{filters}}"
AdvSceneSwitcher.action.filter.entry.settings="{{settings}}{{settingsInputMethod}}{{settingValue}}{{tempVar}}"
AdvSceneSwitcher.action.filter.getSettings="Get current settings"
AdvSceneSwitcher.action.filter.inputMethod.individualManual="Set to fixed value"
AdvSceneSwitcher.action.filter.inputMethod.individualTempvar="Set to macro property"
AdvSceneSwitcher.action.filter.inputMethod.json="Set setting JSON string"
AdvSceneSwitcher.action.source="Source"
AdvSceneSwitcher.action.source.type.enable="Enable"
AdvSceneSwitcher.action.source.type.disable="Disable"
@@ -569,8 +641,11 @@ AdvSceneSwitcher.action.source.type.refreshSettings.tooltip="Can be used to refr
AdvSceneSwitcher.action.source.type.deinterlaceMode="Set deinterlace mode"
AdvSceneSwitcher.action.source.type.deinterlaceOrder="Set deinterlace field order"
AdvSceneSwitcher.action.source.type.openInteractionDialog="Open interaction dialog"
AdvSceneSwitcher.action.source.type.openFilterDialog="Open filter dialog"
AdvSceneSwitcher.action.source.type.openPropertiesDialog="Open properties dialog"
AdvSceneSwitcher.action.source.noSettingsButtons="No buttons found!"
AdvSceneSwitcher.action.source.entry="{{actions}}{{sources}}{{settingsButtons}}{{deinterlaceMode}}{{deinterlaceOrder}}"
AdvSceneSwitcher.action.source.entry.settings="{{settings}}{{settingsInputMethod}}{{settingValue}}{{tempVar}}"
AdvSceneSwitcher.action.source.warning="Warning: Enabling and disabling sources globally cannot be controlled by the OBS UI\nYou might be looking for \"Scene item visibility\""
AdvSceneSwitcher.action.source.getSettings="Get current settings"
AdvSceneSwitcher.action.source.deinterlaceMode.disable="Disable"
@@ -584,6 +659,9 @@ AdvSceneSwitcher.action.source.deinterlaceMode.yadif="Yadif"
AdvSceneSwitcher.action.source.deinterlaceMode.yadif2x="Yadif 2x"
AdvSceneSwitcher.action.source.deinterlaceOrder.topFieldFirst="Top Field First"
AdvSceneSwitcher.action.source.deinterlaceOrder.bottomFieldFirst="Bottom Field First"
AdvSceneSwitcher.action.source.inputMethod.individualManual="Set to fixed value"
AdvSceneSwitcher.action.source.inputMethod.individualTempvar="Set to macro property"
AdvSceneSwitcher.action.source.inputMethod.json="Set setting JSON string"
AdvSceneSwitcher.action.media="Media"
AdvSceneSwitcher.action.media.type.play="Play"
AdvSceneSwitcher.action.media.type.pause="Pause"
@@ -591,8 +669,10 @@ AdvSceneSwitcher.action.media.type.stop="Stop"
AdvSceneSwitcher.action.media.type.restart="Restart"
AdvSceneSwitcher.action.media.type.next="Next"
AdvSceneSwitcher.action.media.type.previous="Previous"
AdvSceneSwitcher.action.media.type.seek="Seek to"
AdvSceneSwitcher.action.media.entry="{{actions}}{{duration}}{{mediaSources}}"
AdvSceneSwitcher.action.media.type.seek.duration="Seek to duration"
AdvSceneSwitcher.action.media.type.seek.percentage="Seek to percentage"
AdvSceneSwitcher.action.media.entry="{{actions}}{{seekDuration}}{{seekPercentage}}{{mediaSources}}"
AdvSceneSwitcher.action.media.seek.percentage.label="Percentage of total duration of the media"
AdvSceneSwitcher.action.macro="Macro"
AdvSceneSwitcher.action.macro.type.pause="Pause"
AdvSceneSwitcher.action.macro.type.unpause="Unpause"
@@ -740,6 +820,9 @@ AdvSceneSwitcher.action.variable.type.mathExpression="Mathematical expression"
AdvSceneSwitcher.action.variable.type.askForValue="Get user input"
AdvSceneSwitcher.action.variable.type.environmentVariable="Set to environment variable value"
AdvSceneSwitcher.action.variable.type.sceneItemCount="Set to scene item count of scene"
AdvSceneSwitcher.action.variable.type.stringLength="Set to length of string"
AdvSceneSwitcher.action.variable.type.extractJson="Extract json field with name"
AdvSceneSwitcher.action.variable.type.setToTempvar="Set to macro property"
AdvSceneSwitcher.action.variable.askForValuePromptDefault="Assign value to variable \"%1\":"
AdvSceneSwitcher.action.variable.askForValuePrompt="Assign value to variable:"
AdvSceneSwitcher.action.variable.mathExpression.example="( 1 + 2 * 3 ) / 4"
@@ -751,7 +834,7 @@ AdvSceneSwitcher.action.variable.invalidSelection="Invalid selection!"
AdvSceneSwitcher.action.variable.actionNoVariableSupport="Getting variable values from %1 actions is not supported!"
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="Getting variable values from %1 conditions is not supported!"
AdvSceneSwitcher.action.variable.currentSegmentValue="Current value:"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}{{tempVars}}"
AdvSceneSwitcher.action.variable.entry.substringIndex="Substring start:{{subStringStart}}Substring size:{{subStringSize}}"
AdvSceneSwitcher.action.variable.entry.substringRegex="Assign value of{{regexMatchIdx}}match using regular expression:"
AdvSceneSwitcher.action.variable.entry.findAndReplace="{{findStr}}{{replaceStr}}"
@@ -778,17 +861,34 @@ AdvSceneSwitcher.action.sceneLock.type.unlock="unlock"
AdvSceneSwitcher.action.sceneLock.type.toggle="toggle lock of"
AdvSceneSwitcher.action.sceneLock.entry="On{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.twitch="Twitch"
AdvSceneSwitcher.action.twitch.type.title="Set stream title"
AdvSceneSwitcher.action.twitch.type.category="Set stream category"
AdvSceneSwitcher.action.twitch.type.marker="Create stream marker"
AdvSceneSwitcher.action.twitch.type.clip="Create stream clip"
AdvSceneSwitcher.action.twitch.type.commercial="Start commercial with duration"
AdvSceneSwitcher.action.twitch.type.channel.info.title.set="Set stream title"
AdvSceneSwitcher.action.twitch.type.channel.info.category.set="Set stream category"
AdvSceneSwitcher.action.twitch.type.raid.start="Start raid"
AdvSceneSwitcher.action.twitch.type.commercial.start="Start commercial"
AdvSceneSwitcher.action.twitch.type.marker.create="Create stream marker"
AdvSceneSwitcher.action.twitch.type.clip.create="Create stream clip"
AdvSceneSwitcher.action.twitch.type.chat.announcement.send="Send chat announcement"
AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable="Enable chat's emote-only mode"
AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable="Disable chat's emote-only mode"
AdvSceneSwitcher.action.twitch.type.sendChatMessage="Send chat message"
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!"
AdvSceneSwitcher.action.twitch.entry="On{{account}}{{actions}}{{streamTitle}}{{category}}{{manualCategorySearch}}{{markerDescription}}{{clipHasDelay}}{{duration}}"
AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient="Permissions of selected token are insufficient to perform selected action!"
AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip"
AdvSceneSwitcher.action.twitch.marker.description="Describe marker"
AdvSceneSwitcher.action.twitch.entry.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}"
AdvSceneSwitcher.action.twitch.entry.chat="Using account{{account}}{{actions}}on{{channel}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}"
AdvSceneSwitcher.action.twitch.title.title="Enter title"
AdvSceneSwitcher.action.twitch.marker.description="Describe marker"
AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip"
AdvSceneSwitcher.action.twitch.announcement.message="Enter message"
AdvSceneSwitcher.action.twitch.announcement.primary="Primary (channel's accent color)"
AdvSceneSwitcher.action.twitch.announcement.blue="Blue"
AdvSceneSwitcher.action.twitch.announcement.green="Green"
AdvSceneSwitcher.action.twitch.announcement.orange="Orange"
AdvSceneSwitcher.action.twitch.announcement.purple="Purple"
AdvSceneSwitcher.action.clipboard="Clipboard"
AdvSceneSwitcher.action.clipboard.type.copy.text="Copy text"
AdvSceneSwitcher.action.clipboard.type.copy.image="Copy image"
AdvSceneSwitcher.action.clipboard.copy.text.text.placeholder="Enter text"
AdvSceneSwitcher.action.clipboard.copy.image.url.placeholder="Enter direct image URL"
AdvSceneSwitcher.action.clipboard.copy.image.url.tooltip="Currently supported formats: PNG, JPG/JPEG, BMP, GIF."
; Hotkey
AdvSceneSwitcher.hotkey.startSwitcherHotkey="Start the Advanced Scene Switcher"
@@ -819,6 +919,8 @@ AdvSceneSwitcher.item.nameReserved="Name reserved!"
AdvSceneSwitcher.macroSegmentSelection.invalid="Invalid selection!"
AdvSceneSwitcher.validation.text.maxLength="Only %1 characters are allowed in this field!"
AdvSceneSwitcher.variable.select="--select variable--"
AdvSceneSwitcher.variable.add="Add new variable"
AdvSceneSwitcher.variable.configure="Configure variable settings"
@@ -830,6 +932,8 @@ AdvSceneSwitcher.variable.save.dontSave="Don't save variable value"
AdvSceneSwitcher.variable.save.save="Save variable value"
AdvSceneSwitcher.variable.save.default="Set to value"
AdvSceneSwitcher.tooltip.availableVariables="Variables are supported, use ${VarName} to retrieve the value of VarName"
AdvSceneSwitcher.connection.select="--select connection--"
AdvSceneSwitcher.connection.add="Add new connection"
AdvSceneSwitcher.connection.configure="Configure connection settings"
@@ -927,21 +1031,53 @@ AdvSceneSwitcher.twitchToken.add="Add new connection"
AdvSceneSwitcher.twitchToken.configure="Configure Twitch connection settings"
AdvSceneSwitcher.twitchToken.value="Token:"
AdvSceneSwitcher.twitchToken.invalid="Invalid twitch token"
AdvSceneSwitcher.twitchToken.request="Request token"
AdvSceneSwitcher.twitchToken.request="Connect Account"
AdvSceneSwitcher.twitchToken.request.waiting="Waiting for token approval ..."
AdvSceneSwitcher.twitchToken.request.fail="Failed to get token!"
AdvSceneSwitcher.twitchToken.request.fail.browser="Authentication failed! (%1)\nYou can close this window now."
AdvSceneSwitcher.twitchToken.request.fail.stateMismatch="State mismatch"
AdvSceneSwitcher.twitchToken.request.success="Successfully received token!"
AdvSceneSwitcher.twitchToken.request.success.browser="Authentication successful! You can close this window now."
AdvSceneSwitcher.twitchToken.request.notSet="No token set - Please request new token!"
AdvSceneSwitcher.twitchToken.request.notSet="Account is not connected!"
AdvSceneSwitcher.twitchToken.permissions="Token permissions:"
AdvSceneSwitcher.twitchToken.analytics.readExtensions="View analytics data for the Twitch Extensions owned by the authenticated account."
AdvSceneSwitcher.twitchToken.analytics.readGames="View analytics data for the games owned by the authenticated account."
AdvSceneSwitcher.twitchToken.bits.read="View Bits information for a channel."
AdvSceneSwitcher.twitchToken.channel.manageBroadcast="Manage a channels broadcast configuration, including updating channel configuration and managing stream markers and stream tags."
AdvSceneSwitcher.twitchToken.channel.startCommercial="Run commercials on a channel."
AdvSceneSwitcher.twitchToken.channel.createClip="Create clips from channel's broadcasts."
AdvSceneSwitcher.twitchToken.permissionsInsufficient="Permissions of selected token are insufficient to perform selected action!"
AdvSceneSwitcher.twitchToken.channel.broadcast.manage="Manage a channels broadcast configuration, including updating channel configuration and managing stream markers and stream tags."
AdvSceneSwitcher.twitchToken.channel.subscriptions.read="Read channel's subscribers list and info."
AdvSceneSwitcher.twitchToken.moderator.followers.read="Read the followers of a broadcaster."
AdvSceneSwitcher.twitchToken.bits.read="Read channel's Bits information."
AdvSceneSwitcher.twitchToken.channel.raids.manage="Manage channel's raids."
AdvSceneSwitcher.twitchToken.moderator.shoutouts.read="Read channel's shoutouts."
AdvSceneSwitcher.twitchToken.moderator.shoutouts.manage="Manage channel's shoutouts."
AdvSceneSwitcher.twitchToken.channel.polls.read="Read channel's polls' state."
AdvSceneSwitcher.twitchToken.channel.polls.manage="Manage channel's polls."
AdvSceneSwitcher.twitchToken.channel.predictions.read="Read channel's predictions' state."
AdvSceneSwitcher.twitchToken.channel.predictions.manage="Manage channel's predictions."
AdvSceneSwitcher.twitchToken.channel.goals.read="Read channel's creator goals."
AdvSceneSwitcher.twitchToken.channel.hypeTrain.read="Read channel's Hype Train state."
AdvSceneSwitcher.twitchToken.channel.charity.read="Read channel's charity campaigns' state and donations."
AdvSceneSwitcher.twitchToken.moderator.shieldMode.read="Read channel's Shield Mode state."
AdvSceneSwitcher.twitchToken.moderator.shieldMode.manage="Manage channel's Shield Mode."
AdvSceneSwitcher.twitchToken.channel.redemptions.read="Read channel's point rewards and their redemptions."
AdvSceneSwitcher.twitchToken.channel.redemptions.manage="Manage channel's point rewards and their redemptions."
AdvSceneSwitcher.twitchToken.channel.moderate="Moderate the channel."
AdvSceneSwitcher.twitchToken.moderator.bannedUsers.manage="Manage channel's banned users."
AdvSceneSwitcher.twitchToken.user.blockedUsers.manage="Manage user's blocked users."
AdvSceneSwitcher.twitchToken.moderation.read="Read channel's moderation data."
AdvSceneSwitcher.twitchToken.channel.moderators.manage="Manage channel's moderators."
AdvSceneSwitcher.twitchToken.channel.vips.manage="Manage channel's VIPs."
AdvSceneSwitcher.twitchToken.channel.commercial.edit="Run commercials on a channel."
AdvSceneSwitcher.twitchToken.channel.ads.manage="Manage channel's commercials."
AdvSceneSwitcher.twitchToken.clips.edit="Create clips from channel's broadcasts."
AdvSceneSwitcher.twitchToken.moderator.announcements.manage="Manage channel's chat announcements."
AdvSceneSwitcher.twitchToken.moderator.chat.settings.manage="Manage channel's chat settings, such as emote-only or slow mode."
AdvSceneSwitcher.twitchToken.user.whispers.manage="Manage user's whispers."
AdvSceneSwitcher.twitchToken.chat.read="View live stream chat messages."
AdvSceneSwitcher.twitchToken.chat.edit="Send live stream chat messages."
AdvSceneSwitcher.twitch.selection.channel.open="Open channel"
AdvSceneSwitcher.twitch.selection.channel.open.tooltip.details="Open channel in external application handling the HTTPS protocol."
AdvSceneSwitcher.twitch.selection.channel.open.tooltip.noAccount="Unable to check if channel exists without selecting a Twitch account first!"
AdvSceneSwitcher.twitch.selection.channel.open.tooltip.noChannel="Channel with specified name was not found!"
AdvSceneSwitcher.twitchCategories.fetchStart="Fetching stream categories ..."
AdvSceneSwitcher.twitchCategories.fetchStatus="Got %1 stream categories."
@@ -955,6 +1091,424 @@ AdvSceneSwitcher.twitchCategories.searchFailed="No new categories were found for
AdvSceneSwitcher.twitchCategories.searchSuccess="%1 new categories were found for \"%2\" and were added to the list!"
AdvSceneSwitcher.twitchCategories.select="--select category--"
AdvSceneSwitcher.twitch.selection.points.reward.placeholder="--select points reward--"
AdvSceneSwitcher.twitch.selection.points.reward.refresh="Manually refresh the list"
AdvSceneSwitcher.twitch.selection.points.reward.option.any="Any"
AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noAccount="Can't select points reward without selecting a Twitch account first!"
AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noPermission="Can't select points reward because of insufficient token permissions!"
AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noChannel="Can't select points reward without entering a channel first!"
AdvSceneSwitcher.twitch.selection.points.reward.tooltip.error="Can't select points reward because Twitch responded with an error! Check OBS logs for more details."
AdvSceneSwitcher.tempVar.select="--select value--"
AdvSceneSwitcher.tempVar.selectionInfo.lastValues="Last values:"
AdvSceneSwitcher.tempVar.twitch.broadcaster_user_id="Twitch broadcaster user id"
AdvSceneSwitcher.tempVar.twitch.broadcaster_user_id.description="The numerical Twitch user ID of the broadcaster."
AdvSceneSwitcher.tempVar.twitch.broadcaster_user_login="Twitch broadcaster user login"
AdvSceneSwitcher.tempVar.twitch.broadcaster_user_login.description="The Twitch user login of the broadcaster"
AdvSceneSwitcher.tempVar.twitch.broadcaster_user_name="Twitch broadcaster user name"
AdvSceneSwitcher.tempVar.twitch.broadcaster_user_name.description="The Twitch user name of the broadcaster"
AdvSceneSwitcher.tempVar.twitch.user_id.follow="Follower Twitch user id"
AdvSceneSwitcher.tempVar.twitch.user_id.follow.description="The user ID for the user now following the specified channel."
AdvSceneSwitcher.tempVar.twitch.user_login.follow="Follower Twitch user login"
AdvSceneSwitcher.tempVar.twitch.user_login.follow.description="The user login for the user now following the specified channel."
AdvSceneSwitcher.tempVar.twitch.user_name.follow="Follower Twitch user name"
AdvSceneSwitcher.tempVar.twitch.user_name.follow.description="The user display name for the user now following the specified channel."
AdvSceneSwitcher.tempVar.twitch.followed_at="Twitch follow timestamp"
AdvSceneSwitcher.tempVar.twitch.followed_at.description="The timestamp of when the Twitch follow event occurred"
AdvSceneSwitcher.tempVar.twitch.id.stream.online="Twitch stream id"
AdvSceneSwitcher.tempVar.twitch.type.stream.online="Twitch stream type"
AdvSceneSwitcher.tempVar.twitch.type.stream.online.description="The stream type. Valid values are: live, playlist, watch_party, premiere, rerun."
AdvSceneSwitcher.tempVar.twitch.started_at.stream.online="Twitch stream start timestamp"
AdvSceneSwitcher.tempVar.twitch.started_at.stream.online.description="The timestamp at which the stream went online at."
AdvSceneSwitcher.tempVar.twitch.title="Twitch stream title"
AdvSceneSwitcher.tempVar.twitch.language="Twitch stream language code"
AdvSceneSwitcher.tempVar.twitch.language.description="The broadcasters preferred language. The value is an ISO 639-1 two-letter language code (for example, en for English). The value is set to “other” if the language is not a Twitch supported language."
AdvSceneSwitcher.tempVar.twitch.category_id="Twitch stream category id"
AdvSceneSwitcher.tempVar.twitch.category_id.description="Numerical id identifying each Twitch streaming category"
AdvSceneSwitcher.tempVar.twitch.category_name="Twitch stream category name"
AdvSceneSwitcher.tempVar.twitch.content_classification_labels="Twitch stream classification labels"
AdvSceneSwitcher.tempVar.twitch.user_id.subscribe="Subscriber Twitch user id"
AdvSceneSwitcher.tempVar.twitch.user_id.subscribe.description="The user ID for the user now subscribed to the specified channel."
AdvSceneSwitcher.tempVar.twitch.user_login.subscribe="Subscriber Twitch user login"
AdvSceneSwitcher.tempVar.twitch.user_login.subscribe.description="The user login for the user now subscribed to the specified channel."
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe="Subscriber Twitch user name"
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe.description="The user display name for the user now subscribed to the specified channel."
AdvSceneSwitcher.tempVar.twitch.tier.subscribe="Twitch subscriber tier"
AdvSceneSwitcher.tempVar.twitch.tier.subscribe.description="The tier of the subscription. Valid values are 1000, 2000, and 3000."
AdvSceneSwitcher.tempVar.twitch.is_gift.subscribe="Is gifted Twitch subscription"
AdvSceneSwitcher.tempVar.twitch.is_gift.subscribe.description="Valid values are true and false."
AdvSceneSwitcher.tempVar.twitch.user_id.subscribe.end="Subscriber Twitch user id"
AdvSceneSwitcher.tempVar.twitch.user_id.subscribe.end.description="The user ID for the user no longer subscribed to the specified channel."
AdvSceneSwitcher.tempVar.twitch.user_login.subscribe.end="Subscriber Twitch user login"
AdvSceneSwitcher.tempVar.twitch.user_login.subscribe.end.description="The user login for the user no longer subscribed to the specified channel."
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe.end="Subscriber Twitch user name"
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe.end.description="The user display name for the user no longer subscribed to the specified channel."
AdvSceneSwitcher.tempVar.twitch.is_gift.subscribe="Was gifted Twitch subscription"
AdvSceneSwitcher.tempVar.twitch.is_gift.subscribe.description="Valid values are true and false."
AdvSceneSwitcher.tempVar.twitch.user_id.subscribe.gift="Subscriber Twitch user id"
AdvSceneSwitcher.tempVar.twitch.user_id.subscribe.gift.description="The user ID of the user who sent the subscription gift. Set to null if it was an anonymous subscription gift."
AdvSceneSwitcher.tempVar.twitch.user_login.subscribe.gift="Subscriber Twitch user login"
AdvSceneSwitcher.tempVar.twitch.user_login.subscribe.gift.description="The user login of the user who sent the gift. Set to null if it was an anonymous subscription gift."
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe.gift="Subscriber Twitch user name"
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe.gift.description="The user display name of the user who sent the gift. Set to null if it was an anonymous subscription gift."
AdvSceneSwitcher.tempVar.twitch.total.subscribe="Number of subscriptions"
AdvSceneSwitcher.tempVar.twitch.total.subscribe.description="The number of subscriptions in the subscription gift."
AdvSceneSwitcher.tempVar.twitch.cumulative_total.subscribe="Total gifted subscriptions"
AdvSceneSwitcher.tempVar.twitch.cumulative_total.subscribe.description="The number of subscriptions gifted by this user in the channel. This value is null for anonymous gifts or if the gifter has opted out of sharing this information."
AdvSceneSwitcher.tempVar.twitch.is_anonymous.subscribe="Was anonymous Twitch subscription gift"
AdvSceneSwitcher.tempVar.twitch.is_anonymous.subscribe.description="Valid values are true and false."
AdvSceneSwitcher.tempVar.twitch.user_id.subscribe.message="Subscriber Twitch user id"
AdvSceneSwitcher.tempVar.twitch.user_id.subscribe.message.description="The user ID of the user who sent a resubscription chat message."
AdvSceneSwitcher.tempVar.twitch.user_login.subscribe.message="Subscriber Twitch user login"
AdvSceneSwitcher.tempVar.twitch.user_login.subscribe.message.description="The user login of the user who sent a resubscription chat message."
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe.message="Subscriber Twitch user name"
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe.message.description="The user display name of the user who a resubscription chat message."
AdvSceneSwitcher.tempVar.twitch.message.subscribe="Subscription message"
AdvSceneSwitcher.tempVar.twitch.message.subscribe.description="An object that contains the resubscription message and emote information needed to recreate the message."
AdvSceneSwitcher.tempVar.twitch.cumulative_months.subscribe="Number of months subscribed"
AdvSceneSwitcher.tempVar.twitch.cumulative_months.subscribe.description="The total number of months the user has been subscribed to the channel."
AdvSceneSwitcher.tempVar.twitch.streak_months.subscribe="Subscription streak"
AdvSceneSwitcher.tempVar.twitch.streak_months.subscribe.description="The number of consecutive months the users current subscription has been active. This value is null if the user has opted out of sharing this information."
AdvSceneSwitcher.tempVar.twitch.duration_months.subscribe="Subscription duration"
AdvSceneSwitcher.tempVar.twitch.duration_months.subscribe.description="The month duration of the subscription."
AdvSceneSwitcher.tempVar.twitch.user_id.cheer="Cheerer Twitch user id"
AdvSceneSwitcher.tempVar.twitch.user_id.cheer.description="The user ID for the user who cheered on the specified channel. This is null if is_anonymous is true."
AdvSceneSwitcher.tempVar.twitch.user_login.cheer="Cheerer Twitch user login"
AdvSceneSwitcher.tempVar.twitch.user_login.cheer.description="The user login for the user who cheered on the specified channel. This is null if is_anonymous is true."
AdvSceneSwitcher.tempVar.twitch.user_name.cheer="Cheerer Twitch user name"
AdvSceneSwitcher.tempVar.twitch.user_name.cheer.description="The user display name for the user who cheered on the specified channel. This is null if is_anonymous is true."
AdvSceneSwitcher.tempVar.twitch.is_anonymous.cheer="Is anonymous cheer"
AdvSceneSwitcher.tempVar.twitch.is_anonymous.cheer.description="Whether the user cheered anonymously or not."
AdvSceneSwitcher.tempVar.twitch.message.cheer="Cheer message"
AdvSceneSwitcher.tempVar.twitch.message.cheer.description="The message sent with the cheer."
AdvSceneSwitcher.tempVar.twitch.bits="Bits amount"
AdvSceneSwitcher.tempVar.twitch.bits.description="The number of bits cheered."
AdvSceneSwitcher.tempVar.twitch.id.poll="Poll ID"
AdvSceneSwitcher.tempVar.twitch.id.poll.description="ID of the poll."
AdvSceneSwitcher.tempVar.twitch.title.poll="Poll title"
AdvSceneSwitcher.tempVar.twitch.title.poll.description="Question displayed for the poll."
AdvSceneSwitcher.tempVar.twitch.choices.poll="Choices"
AdvSceneSwitcher.tempVar.twitch.choices.poll.description="An array of choices for the poll."
AdvSceneSwitcher.tempVar.twitch.choices.pollEnd="Choices"
AdvSceneSwitcher.tempVar.twitch.choices.pollEnd.description="An array of choices for the poll. Includes vote counts."
AdvSceneSwitcher.tempVar.twitch.channel_points_voting.description="Channel points voting setting"
AdvSceneSwitcher.tempVar.twitch.channel_points_voting.description="The Channel Points voting settings for the poll."
AdvSceneSwitcher.tempVar.twitch.started_at.poll="Poll start time"
AdvSceneSwitcher.tempVar.twitch.ends_at.poll="Poll end time"
AdvSceneSwitcher.tempVar.twitch.status.poll="Poll status"
AdvSceneSwitcher.tempVar.twitch.status.poll.description="The status of the poll. Valid values are completed, archived, and terminated."
AdvSceneSwitcher.tempVar.twitch.id.prediction="Prediction ID"
AdvSceneSwitcher.tempVar.twitch.id.prediction.description="Channel Points Prediction ID."
AdvSceneSwitcher.tempVar.twitch.title.prediction="Prediction title"
AdvSceneSwitcher.tempVar.twitch.title.prediction.description="Title for the Channel Points Prediction."
AdvSceneSwitcher.tempVar.twitch.outcomes.prediction="Prediction outcomes"
AdvSceneSwitcher.tempVar.twitch.outcomes.prediction.description="An array of outcomes for the Channel Points Prediction. Includes top_predictors."
AdvSceneSwitcher.tempVar.twitch.started_at.prediction="Prediction start time"
AdvSceneSwitcher.tempVar.twitch.locks_at.prediction="Prediction lock time"
AdvSceneSwitcher.tempVar.twitch.ended_at.prediction="Prediction end time"
AdvSceneSwitcher.tempVar.twitch.status.prediction="Prediction status"
AdvSceneSwitcher.tempVar.twitch.status.prediction.description="The status of the Channel Points Prediction. Valid values are resolved and canceled."
AdvSceneSwitcher.tempVar.twitch.id.goal="Goal ID"
AdvSceneSwitcher.tempVar.twitch.type.goal="Goal type"
AdvSceneSwitcher.tempVar.twitch.type.goal.description="The type of goal. Possible values are:\n
follow — The goal is to increase followers.\n
subscription — The goal is to increase subscriptions. This type shows the net increase or decrease in tier points associated with the subscriptions.\n
subscription_count — The goal is to increase subscriptions. This type shows the net increase or decrease in the number of subscriptions.\n
new_subscription — The goal is to increase subscriptions. This type shows only the net increase in tier points associated with the subscriptions (it does not account for users that unsubscribed since the goal started).\n
new_subscription_count — The goal is to increase subscriptions. This type shows only the net increase in the number of subscriptions (it does not account for users that unsubscribed since the goal started)."
AdvSceneSwitcher.tempVar.twitch.description.goal="Goal description"
AdvSceneSwitcher.tempVar.twitch.description.goal.description="A description of the goal, if specified. The description may contain a maximum of 40 characters."
AdvSceneSwitcher.tempVar.twitch.is_achieved.goal="Goal is achieved"
AdvSceneSwitcher.tempVar.twitch.is_achieved.goal.description="A Boolean value that indicates whether the broadcaster achieved their goal. Is true if the goal was achieved; otherwise, false."
AdvSceneSwitcher.tempVar.twitch.current_amount.goal="Goal current amount"
AdvSceneSwitcher.tempVar.twitch.current_amount.goal.description="The goal's current value.\n
The goal's type determines how this value is increased or decreased.\n
If type is follow, this field is set to the broadcaster's current number of followers. This number increases with new followers and decreases when users unfollow the broadcaster.\n
If type is subscription, this field is increased and decreased by the points value associated with the subscription tier. For example, if a tier-two subscription is worth 2 points, this field is increased or decreased by 2, not 1.\n
If type is subscription_count, this field is increased by 1 for each new subscription and decreased by 1 for each user that unsubscribes.\n
If type is new_subscription, this field is increased by the points value associated with the subscription tier. For example, if a tier-two subscription is worth 2 points, this field is increased by 2, not 1.\n
If type is new_subscription_count, this field is increased by 1 for each new subscription."
AdvSceneSwitcher.tempVar.twitch.target_amount.goal="Goal target amount"
AdvSceneSwitcher.tempVar.twitch.target_amount.goal.description="The goal's target value. For example, if the broadcaster has 200 followers before creating the goal, and their goal is to double that number, this field is set to 400."
AdvSceneSwitcher.tempVar.twitch.started_at.goal="Goal start time"
AdvSceneSwitcher.tempVar.twitch.ended_at.goal="Goal end time"
AdvSceneSwitcher.tempVar.twitch.id.hype="Hype train ID"
AdvSceneSwitcher.tempVar.twitch.total.hype="Hype train total points"
AdvSceneSwitcher.tempVar.twitch.total.hype.description="Total points contributed to the Hype Train."
AdvSceneSwitcher.tempVar.twitch.progress.hype="Hype train progress"
AdvSceneSwitcher.tempVar.twitch.progress.hype.description="The number of points contributed to the Hype Train at the current level."
AdvSceneSwitcher.tempVar.twitch.goal.hype="Hype train goal"
AdvSceneSwitcher.tempVar.twitch.goal.hype.description="The number of points required to reach the next level."
AdvSceneSwitcher.tempVar.twitch.top_contributions.hype="Hype train top contributors"
AdvSceneSwitcher.tempVar.twitch.top_contributions.hype.description="The contributors with the most points contributed."
AdvSceneSwitcher.tempVar.twitch.last_contribution.hype="Hype train last contributor"
AdvSceneSwitcher.tempVar.twitch.last_contribution.hype.description="The most recent contribution."
AdvSceneSwitcher.tempVar.twitch.level.hype="Hype train level"
AdvSceneSwitcher.tempVar.twitch.started_at.hype="Hype train start time"
AdvSceneSwitcher.tempVar.twitch.expires_at.hype="Hype train expiration time"
AdvSceneSwitcher.tempVar.twitch.ended_at.hype="Hype train end time"
AdvSceneSwitcher.tempVar.twitch.cooldown_ends_at.hype="Hype train cooldown"
AdvSceneSwitcher.tempVar.twitch.cooldown_ends_at.hype.description="The time when the Hype Train cooldown ends so that the next Hype Train can start."
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_id.shoutout="User ID (shouted out)"
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_id.shoutout.description="An ID that identifies the broadcaster that received the Shoutout."
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_login.shoutout="User login (shouted out)"
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_login.shoutout.description="The user login that identifies the broadcaster that received the Shoutout."
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_name.shoutout="User name (shouted out)"
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_name.shoutout.description="The user name that identifies the broadcaster that received the Shoutout."
AdvSceneSwitcher.tempVar.twitch.moderator_user_id.shoutout="User ID triggering shouting out"
AdvSceneSwitcher.tempVar.twitch.moderator_user_id.shoutout.description="An ID that identifies the moderator that sent the Shoutout. If the broadcaster sent the Shoutout, this ID is the same as the ID in broadcaster_user_id."
AdvSceneSwitcher.tempVar.twitch.moderator_user_login.shoutout="User login triggering shouting out"
AdvSceneSwitcher.tempVar.twitch.moderator_user_login.shoutout.description="The user login that identifies the moderator that sent the Shoutout. If the broadcaster sent the Shoutout, this ID is the same as the ID in broadcaster_user_id."
AdvSceneSwitcher.tempVar.twitch.moderator_user_name.shoutout="User name triggering shouting out"
AdvSceneSwitcher.tempVar.twitch.moderator_user_name.shoutout.description="The user login that identifies the moderator that sent the Shoutout. If the broadcaster sent the Shoutout, this ID is the same as the ID in broadcaster_user_id."
AdvSceneSwitcher.tempVar.twitch.viewer_count.shoutout="Viewer count"
AdvSceneSwitcher.tempVar.twitch.viewer_count.shoutout.description="The number of users that were watching the broadcaster's stream at the time of the Shoutout."
AdvSceneSwitcher.tempVar.twitch.started_at.shoutout="Shoutout start time"
AdvSceneSwitcher.tempVar.twitch.started_at.shoutout.description="The UTC timestamp (in RFC3339 format) of when the moderator sent the Shoutout."
AdvSceneSwitcher.tempVar.twitch.cooldown_ends_at.shoutout="Shoutout cooldown end time"
AdvSceneSwitcher.tempVar.twitch.cooldown_ends_at.shoutout.description="The UTC timestamp (in RFC3339 format) of when the broadcaster may send a Shoutout to a different broadcaster."
AdvSceneSwitcher.tempVar.twitch.target_cooldown_ends_at.shoutout="Shoutout target cooldown end time"
AdvSceneSwitcher.tempVar.twitch.target_cooldown_ends_at.shoutout.description="The UTC timestamp (in RFC3339 format) of when the broadcaster may send another Shoutout to the selected broadcaster."
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_id.shoutoutReceived="User ID shouting out"
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_id.shoutoutReceived.description="An ID that identifies the broadcaster that received the Shoutout."
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_login.shoutoutReceived="User login shouting out"
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_login.shoutoutReceived.description="The user login that identifies the broadcaster that received the Shoutout."
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_name.shoutoutReceived="User name shouting out"
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_name.shoutoutReceived.description="The user name that identifies the broadcaster that received the Shoutout."
AdvSceneSwitcher.tempVar.twitch.viewer_count.shoutoutReceived="Viewer count"
AdvSceneSwitcher.tempVar.twitch.viewer_count.shoutoutReceived.description="The number of users that were watching the from-broadcaster's stream at the time of the Shoutout."
AdvSceneSwitcher.tempVar.twitch.started_at.shoutoutReceived="Shoutout time"
AdvSceneSwitcher.tempVar.twitch.started_at.shoutoutReceived.description="The UTC timestamp (in RFC3339 format) of when the moderator sent the Shoutout."
AdvSceneSwitcher.tempVar.twitch.id.reward="Reward id"
AdvSceneSwitcher.tempVar.twitch.user_id.reward="User id redeeming reward"
AdvSceneSwitcher.tempVar.twitch.user_login.reward="User login redeeming reward"
AdvSceneSwitcher.tempVar.twitch.user_name.reward="User login redeeming reward"
AdvSceneSwitcher.tempVar.twitch.user_input.reward="User input for reward"
AdvSceneSwitcher.tempVar.twitch.reward.reward="Reward Information"
AdvSceneSwitcher.tempVar.twitch.reward.reward.description="Basic information about the reward that was redeemed, at the time it was redeemed."
AdvSceneSwitcher.tempVar.twitch.status.reward="Reward status"
AdvSceneSwitcher.tempVar.twitch.status.reward.description="Defaults to unfulfilled. Possible values are unknown, unfulfilled, fulfilled, and canceled."
AdvSceneSwitcher.tempVar.twitch.redeemed_at.reward="Redemption time"
AdvSceneSwitcher.tempVar.twitch.redeemed_at.reward.description="RFC3339 timestamp of when the reward was redeemed."
AdvSceneSwitcher.tempVar.twitch.is_enabled.reward="Reward is enabled"
AdvSceneSwitcher.tempVar.twitch.is_enabled.reward.description="Is the reward currently enabled. If false, the reward wont show up to viewers."
AdvSceneSwitcher.tempVar.twitch.is_paused.reward="Reward is paused"
AdvSceneSwitcher.tempVar.twitch.is_paused.reward.description="Is the reward currently paused. If true, viewers cant redeem."
AdvSceneSwitcher.tempVar.twitch.is_in_stock.reward="Reward is in stock"
AdvSceneSwitcher.tempVar.twitch.is_in_stock.reward.description="Is the reward currently in stock. If false, viewers cant redeem."
AdvSceneSwitcher.tempVar.twitch.title.reward="Reward title"
AdvSceneSwitcher.tempVar.twitch.cost.reward="Reward cost"
AdvSceneSwitcher.tempVar.twitch.prompt.reward="Reward description"
AdvSceneSwitcher.tempVar.twitch.is_user_input_required.reward="Reward requires user input"
AdvSceneSwitcher.tempVar.twitch.should_redemptions_skip_request_queue.reward="Redemptions skip request queue"
AdvSceneSwitcher.tempVar.twitch.should_redemptions_skip_request_queue.reward.description="Should redemptions be set to fulfilled status immediately when redeemed and skip the request queue instead of the normal unfulfilled status."
AdvSceneSwitcher.tempVar.twitch.max_per_stream.reward="Reward maximum per stream"
AdvSceneSwitcher.tempVar.twitch.max_per_user_per_stream.reward="Reward maximum per user per stream"
AdvSceneSwitcher.tempVar.twitch.background_color.reward="Reward background color"
AdvSceneSwitcher.tempVar.twitch.image.reward="Reward image"
AdvSceneSwitcher.tempVar.twitch.image.reward.description="Set of custom images of 1x, 2x and 4x sizes for the reward. Can be null if no images have been uploaded."
AdvSceneSwitcher.tempVar.twitch.default_image.reward="Reward default image"
AdvSceneSwitcher.tempVar.twitch.default_image.reward.description="Set of default images of 1x, 2x and 4x sizes for the reward."
AdvSceneSwitcher.tempVar.twitch.global_cooldown.reward="Reward cooldown"
AdvSceneSwitcher.tempVar.twitch.global_cooldown.reward.description="Whether a cooldown is enabled and what the cooldown is in seconds."
AdvSceneSwitcher.tempVar.twitch.cooldown_expires_at.reward="Reward cooldown expiration"
AdvSceneSwitcher.tempVar.twitch.cooldown_expires_at.reward.description="Timestamp of the cooldown expiration. null if the reward isnt on cooldown."
AdvSceneSwitcher.tempVar.twitch.redemptions_redeemed_current_stream.reward="Redemption count"
AdvSceneSwitcher.tempVar.twitch.redemptions_redeemed_current_stream.reward.description="The number of redemptions redeemed during the current live stream. Counts against the max_per_stream limit. null if the broadcasters stream isnt live or max_per_stream isnt enabled."
AdvSceneSwitcher.tempVar.twitch.redemptions_redeemed_current_stream.reward.description="The number of redemptions redeemed during the current live stream. Counts against the max_per_stream limit. null if the broadcasters stream isnt live or max_per_stream isnt enabled."
AdvSceneSwitcher.tempVar.twitch.charity_name="Charity name"
AdvSceneSwitcher.tempVar.twitch.charity_description="Charity description"
AdvSceneSwitcher.tempVar.twitch.charity_logo="Charity logo"
AdvSceneSwitcher.tempVar.twitch.charity_logo.description="A URL to an image of the charitys logo. The images type is PNG and its size is 100px X 100px."
AdvSceneSwitcher.tempVar.twitch.id.charity="Charity id"
AdvSceneSwitcher.tempVar.twitch.id.charity.description="An ID that identifies the donation. The ID is unique across campaigns."
AdvSceneSwitcher.tempVar.twitch.user_id.charity="Donor user id"
AdvSceneSwitcher.tempVar.twitch.user_id.charity.description="An ID that identifies the user that donated to the campaign."
AdvSceneSwitcher.tempVar.twitch.user_login.charity="Donor login name"
AdvSceneSwitcher.tempVar.twitch.user_login.charity.description="The user login that identifies the user that donated to the campaign."
AdvSceneSwitcher.tempVar.twitch.user_name.charity="Donor user name"
AdvSceneSwitcher.tempVar.twitch.user_name.charity.description="The user name that identifies the user that donated to the campaign."
AdvSceneSwitcher.tempVar.twitch.amount.charity="Donation amount"
AdvSceneSwitcher.tempVar.twitch.amount.charity.description="An object that contains the amount of money that the user donated."
AdvSceneSwitcher.tempVar.twitch.current_amount.charity="Current donation amount"
AdvSceneSwitcher.tempVar.twitch.current_amount.charity.description="An object that contains the current amount of donations that the campaign has received."
AdvSceneSwitcher.tempVar.twitch.target_amount.charity="Charity target amount"
AdvSceneSwitcher.tempVar.twitch.target_amount.charity.description="An object that contains the campaigns target fundraising goal."
AdvSceneSwitcher.tempVar.twitch.started_at.charity="Charity start date"
AdvSceneSwitcher.tempVar.twitch.started_at.charity.description="The UTC timestamp (in RFC3339 format) of when the broadcaster started the charity campaign."
AdvSceneSwitcher.tempVar.twitch.stopped_at.charity="Charity start date"
AdvSceneSwitcher.tempVar.twitch.stopped_at.charity.description="The UTC timestamp (in RFC3339 format) of when the broadcaster stopped the campaign."
AdvSceneSwitcher.tempVar.twitch.moderator_user_id.shieldMode="Moderator user ID"
AdvSceneSwitcher.tempVar.twitch.moderator_user_id.shieldMode.description="An ID that identifies the moderator that updated the Shield Modes status. If the broadcaster updated the status, this ID will be the same as the broadcaster's ID"
AdvSceneSwitcher.tempVar.twitch.moderator_user_login.shieldMode="Moderator login name"
AdvSceneSwitcher.tempVar.twitch.moderator_user_login.shieldMode.description="Login name that identifies the moderator that updated the Shield Modes status. If the broadcaster updated the status, this login name will be the same as the broadcaster's login name"
AdvSceneSwitcher.tempVar.twitch.moderator_user_name.shieldMode="Moderator user name"
AdvSceneSwitcher.tempVar.twitch.moderator_user_name.shieldMode.description="User name that identifies the moderator that updated the Shield Modes status. If the broadcaster updated the status, this user name will be the same as the broadcaster's user name"
AdvSceneSwitcher.tempVar.twitch.started_at.shieldMode="Shield Mode start time"
AdvSceneSwitcher.tempVar.twitch.started_at.shieldMode.description="The UTC timestamp (in RFC3339 format) of when the moderator activated Shield Mode."
AdvSceneSwitcher.tempVar.twitch.ended_at.shieldMode="Shield Mode end time"
AdvSceneSwitcher.tempVar.twitch.ended_at.shieldMode.description="The UTC timestamp (in RFC3339 format) of when the moderator deactivated Shield Mode."
AdvSceneSwitcher.tempVar.twitch.game_name="Game name"
AdvSceneSwitcher.tempVar.twitch.game_name.description="The name of the game that the broadcaster is playing or last played. The value is an empty string if the broadcaster has never played a game."
AdvSceneSwitcher.tempVar.twitch.game_id="Game ID"
AdvSceneSwitcher.tempVar.twitch.game_id.description="An ID that uniquely identifies the game that the broadcaster is playing or last played. The value is an empty string if the broadcaster has never played a game."
AdvSceneSwitcher.tempVar.twitch.delay="Stream delay"
AdvSceneSwitcher.tempVar.twitch.delay.description="The value of the broadcasters stream delay setting, in seconds.\nThis fields value defaults to zero unless 1) the request specifies a user access token, 2) the ID in the broadcaster_id query parameter matches the user ID in the access token, and 3) the broadcaster has partner status and they set a non-zero stream delay value."
AdvSceneSwitcher.tempVar.twitch.tags="Stream tags"
AdvSceneSwitcher.tempVar.twitch.tags.description="The tags applied to the channel."
AdvSceneSwitcher.tempVar.twitch.is_mature="Is mature content"
AdvSceneSwitcher.tempVar.twitch.is_mature.description="A Boolean value that indicates whether the stream is meant for mature audiences."
AdvSceneSwitcher.tempVar.twitch.is_branded_content="Is branded content"
AdvSceneSwitcher.tempVar.twitch.is_branded_content.description="Boolean flag indicating if the channel has branded content."
AdvSceneSwitcher.tempVar.twitch.thumbnail_url="Thumbnail URL"
AdvSceneSwitcher.tempVar.twitch.thumbnail_url.description="A URL to an image of a frame from the last 5 minutes of the stream. Replace the width and height placeholders in the URL ({width}x{height}) with the size of the image you want, in pixels."
AdvSceneSwitcher.tempVar.twitch.viewer_count.live="Viewer count"
AdvSceneSwitcher.tempVar.twitch.viewer_count.live.description="The number of users watching the stream."
AdvSceneSwitcher.tempVar.twitch.user_id.ban="Banned user id"
AdvSceneSwitcher.tempVar.twitch.user_id.ban.description="The user ID for the user who was banned on the specified channel."
AdvSceneSwitcher.tempVar.twitch.user_login.ban="Banned login name"
AdvSceneSwitcher.tempVar.twitch.user_login.ban.description="The user login for the user who was banned on the specified channel."
AdvSceneSwitcher.tempVar.twitch.user_name.ban="Banned user name"
AdvSceneSwitcher.tempVar.twitch.user_name.ban.description="The user display name for the user who was banned on the specified channel."
AdvSceneSwitcher.tempVar.twitch.moderator_user_id.ban="Moderator user id"
AdvSceneSwitcher.tempVar.twitch.moderator_user_id.ban.description="The user ID of the issuer of the ban."
AdvSceneSwitcher.tempVar.twitch.moderator_user_login.ban="Moderator login name"
AdvSceneSwitcher.tempVar.twitch.moderator_user_login.ban.description="The user login of the issuer of the ban."
AdvSceneSwitcher.tempVar.twitch.moderator_user_name.ban="Moderator user name"
AdvSceneSwitcher.tempVar.twitch.moderator_user_name.ban.description="The user name of the issuer of the ban."
AdvSceneSwitcher.tempVar.twitch.reason.ban="Ban reason"
AdvSceneSwitcher.tempVar.twitch.ends_at.ban="Ban end date"
AdvSceneSwitcher.tempVar.twitch.ends_at.ban.description="The UTC date and time (in RFC3339 format) of when the timeout ends. Is null if the user was banned instead of put in a timeout."
AdvSceneSwitcher.tempVar.twitch.is_permanent.ban="Is permanent ban"
AdvSceneSwitcher.tempVar.twitch.is_permanent.ban.description="Indicates whether the ban is permanent (true) or a timeout (false). If true, ends_at will be null."
AdvSceneSwitcher.tempVar.twitch.user_id.unban="Banned user id"
AdvSceneSwitcher.tempVar.twitch.user_id.unban.description="The user ID for the user who was unbanned on the specified channel."
AdvSceneSwitcher.tempVar.twitch.user_login.unban="Banned login name"
AdvSceneSwitcher.tempVar.twitch.user_login.unban.description="The user login for the user who was unbanned on the specified channel."
AdvSceneSwitcher.tempVar.twitch.user_name.unban="Banned user name"
AdvSceneSwitcher.tempVar.twitch.user_name.unban.description="The user display name for the user who was unbanned on the specified channel."
AdvSceneSwitcher.tempVar.twitch.moderator_user_id.unban="Moderator user id"
AdvSceneSwitcher.tempVar.twitch.moderator_user_id.unban.description="The user ID of the issuer of the unban."
AdvSceneSwitcher.tempVar.twitch.moderator_user_login.unban="Moderator login name"
AdvSceneSwitcher.tempVar.twitch.moderator_user_login.unban.description="The user login of the issuer of the unban."
AdvSceneSwitcher.tempVar.twitch.moderator_user_name.unban="Moderator user name"
AdvSceneSwitcher.tempVar.twitch.moderator_user_name.unban.description="The user name of the issuer of the unban."
AdvSceneSwitcher.tempVar.twitch.user_id.addMod="User id"
AdvSceneSwitcher.tempVar.twitch.user_id.addMod.description="The user ID of the new moderator."
AdvSceneSwitcher.tempVar.twitch.user_login.addMod="Login name"
AdvSceneSwitcher.tempVar.twitch.user_login.addMod.description="The user login of the new moderator."
AdvSceneSwitcher.tempVar.twitch.user_name.addMod="User name"
AdvSceneSwitcher.tempVar.twitch.user_name.addMod.description="The display name of the new moderator."
AdvSceneSwitcher.tempVar.twitch.user_id.removeMod="User id"
AdvSceneSwitcher.tempVar.twitch.user_id.removeMod.description="The user ID of the removed moderator."
AdvSceneSwitcher.tempVar.twitch.user_login.removeMod="Login name"
AdvSceneSwitcher.tempVar.twitch.user_login.removeMod.description="The user login of the removed moderator."
AdvSceneSwitcher.tempVar.twitch.user_name.removeMod="User name"
AdvSceneSwitcher.tempVar.twitch.user_name.removeMod.description="The display name of the removed moderator."
AdvSceneSwitcher.tempVar.twitch.chatter="User login"
AdvSceneSwitcher.tempVar.twitch.chatter.description="The user login of the person who sent the chat message."
AdvSceneSwitcher.tempVar.twitch.chat_message="Chat message"
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_id.raid="Raid creator user ID"
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_id.raid.description="The broadcaster ID that created the raid."
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_login.raid="Raid creator user login"
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_login.raid.description="The broadcaster login that created the raid."
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_name.raid="Raid creator user name"
AdvSceneSwitcher.tempVar.twitch.from_broadcaster_user_name.raid.description="The broadcaster display name that created the raid."
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_id.raid="Raid receiver user ID"
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_id.raid.description="The broadcaster ID that created the raid."
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_login.raid="Raid receiver user login"
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_login.raid.description="The broadcaster login that created the raid."
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_name.raid="Raid receiver user name"
AdvSceneSwitcher.tempVar.twitch.to_broadcaster_user_name.raid.description="The broadcaster display name that created the raid."
AdvSceneSwitcher.tempVar.twitch.viewers.raid="Raid participants"
AdvSceneSwitcher.tempVar.twitch.viewers.raid.description="The number of viewers in the raid."
AdvSceneSwitcher.tempVar.audio.output_volume="Output volume"
AdvSceneSwitcher.tempVar.audio.output_volume.description="The volume the audio source is outputting."
AdvSceneSwitcher.tempVar.audio.configured_volume="Configured volume"
AdvSceneSwitcher.tempVar.audio.configured_volume.description="The volume level configured for the source."
AdvSceneSwitcher.tempVar.audio.muted="Source muted"
AdvSceneSwitcher.tempVar.audio.sync_offset="Source audio sync offset"
AdvSceneSwitcher.tempVar.audio.monitor="Source audio monitor type"
AdvSceneSwitcher.tempVar.audio.balance="Source audio balance"
AdvSceneSwitcher.tempVar.clipboard.mimeType.primary="Primary clipboard item MIME type"
AdvSceneSwitcher.tempVar.clipboard.mimeType.primary.description="Highest priority MIME type of the current item stored in clipboard, if available."
AdvSceneSwitcher.tempVar.clipboard.mimeType.all="All clipboard item MIME types"
AdvSceneSwitcher.tempVar.clipboard.mimeType.all.description="All MIME types of the current item stored in clipboard separated with space, if available."
AdvSceneSwitcher.tempVar.scene.current="Current scene"
AdvSceneSwitcher.tempVar.scene.previous="Previous scene"
AdvSceneSwitcher.tempVar.scene.preview="Preview scene"
AdvSceneSwitcher.tempVar.window.window="Window title"
AdvSceneSwitcher.tempVar.window.window.description="The window title of the current foreground window."
AdvSceneSwitcher.tempVar.timer.seconds="Seconds"
AdvSceneSwitcher.tempVar.timer.minutes="Minutes"
AdvSceneSwitcher.tempVar.timer.hours="Hours"
AdvSceneSwitcher.tempVar.timer.days="Days"
AdvSceneSwitcher.tempVar.date.year="Year"
AdvSceneSwitcher.tempVar.date.month="Month"
AdvSceneSwitcher.tempVar.date.day="Day"
AdvSceneSwitcher.tempVar.date.hour="Hour"
AdvSceneSwitcher.tempVar.date.minute="Minute"
AdvSceneSwitcher.tempVar.date.second="Second"
AdvSceneSwitcher.tempVar.date.dayOfWeek="Day of week"
AdvSceneSwitcher.tempVar.midi.type="Message type"
AdvSceneSwitcher.tempVar.midi.channel="Channel"
AdvSceneSwitcher.tempVar.midi.note="Note"
AdvSceneSwitcher.tempVar.midi.value1="Value 1"
AdvSceneSwitcher.tempVar.midi.value2="Value 2"
AdvSceneSwitcher.tempVar.macro.runCount="Run count"
AdvSceneSwitcher.tempVar.macro.runCount.description="The number of times a macro was executed."
AdvSceneSwitcher.tempVar.macro.matchedCount="Matched count"
AdvSceneSwitcher.tempVar.macro.matchedCount.description="The number of macros of which the condition state was true."
AdvSceneSwitcher.tempVar.process.name="Process name"
AdvSceneSwitcher.tempVar.recording.durationSeconds="Recording duration"
AdvSceneSwitcher.tempVar.recording.durationSeconds.description="Recording duration in seconds.\nThis value does not change while the recording is paused and will be reset to zero if the recording is stopped."
AdvSceneSwitcher.tempVar.video.patternCount="Pattern count"
AdvSceneSwitcher.tempVar.video.patternCount.description="The number of times the given pattern has been found in a given video input frame."
AdvSceneSwitcher.tempVar.video.objectCount="Object count"
AdvSceneSwitcher.tempVar.video.objectCount.description="The number of objects the given model has identified in a given video input frame."
AdvSceneSwitcher.tempVar.video.brightness="Average brightness"
AdvSceneSwitcher.tempVar.video.brightness.description="The average brightness in a given video input frame in a range from 0 to 1 (dark to bright)."
AdvSceneSwitcher.tempVar.video.text="OCR text"
AdvSceneSwitcher.tempVar.video.text.description="The text detected in a given video input frame."
AdvSceneSwitcher.tempVar.video.color="Average color"
AdvSceneSwitcher.tempVar.video.color.description="The average RGB color in a given video input frame in HexArgb format."
AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
AdvSceneSwitcher.selectCurrentScene="Current Scene"
@@ -980,6 +1534,7 @@ AdvSceneSwitcher.enterText="--enter text--"
AdvSceneSwitcher.enterURL="--enter URL--"
AdvSceneSwitcher.selectHotkey="--select hotkey--"
AdvSceneSwitcher.selectDisplay="--select display--"
AdvSceneSwitcher.selectSetting="--select setting--"
AdvSceneSwitcher.invaildEntriesWillNotBeSaved="invalid entries will not be saved"
AdvSceneSwitcher.selectWindowTip="Use \"OBS\" to specify OBS window\nUse \"Task Switching\"to specify ALT + TAB"
@@ -1001,6 +1556,8 @@ AdvSceneSwitcher.sceneItemSelection.type.all.entry="All sources"
AdvSceneSwitcher.sceneItemSelection.all="All"
AdvSceneSwitcher.sceneItemSelection.any="Any"
AdvSceneSwitcher.filterSelection.all="All filters"
AdvSceneSwitcher.status.active="Active"
AdvSceneSwitcher.status.inactive="Inactive"
AdvSceneSwitcher.running="Plugin running"

View File

@@ -41,17 +41,17 @@ AdvSceneSwitcher.generalTab.saveOrLoadsettings.exportWindowTitle="Exportar los a
AdvSceneSwitcher.generalTab.saveOrLoadsettings.importWindowTitle="Importar los ajustes para Advanced Scene Switcher ..."
AdvSceneSwitcher.generalTab.saveOrLoadsettings.textType="Archivos de texto (*.txt)"
AdvSceneSwitcher.generalTab.saveOrLoadsettings.loadFail="La importacion la configuracion de escenas fallo"
AdvSceneSwitcher.generalTab.saveOrLoadsettings.loadSuccess="Configuración de escenas importada correctamente"
AdvSceneSwitcher.generalTab.priority.fileContent="Contenido del archivo"
AdvSceneSwitcher.generalTab.priority.sceneSequence="Secuencia de escenas"
AdvSceneSwitcher.generalTab.priority.idleDetection="Detección de inactivos"
AdvSceneSwitcher.generalTab.priority.executable="Ejecutable"
AdvSceneSwitcher.generalTab.priority.screenRegion="Región de pantalla"
AdvSceneSwitcher.generalTab.priority.windowTitle="Título de la ventana"
AdvSceneSwitcher.generalTab.priority.media="Medios"
AdvSceneSwitcher.generalTab.priority.time="Tiempo"
AdvSceneSwitcher.generalTab.priority.audio="Audio"
AdvSceneSwitcher.generalTab.priority.video="Video"
AdvSceneSwitcher.generalTab.saveOrLoadsettings.loadSuccess="Configuración de escenas importada correctamente"
AdvSceneSwitcher.generalTab.priority.fileContent="Contenido del archivo"
AdvSceneSwitcher.generalTab.priority.sceneSequence="Secuencia de escenas"
AdvSceneSwitcher.generalTab.priority.idleDetection="Detección de inactivos"
AdvSceneSwitcher.generalTab.priority.executable="Ejecutable"
AdvSceneSwitcher.generalTab.priority.screenRegion="Región de pantalla"
AdvSceneSwitcher.generalTab.priority.windowTitle="Título de la ventana"
AdvSceneSwitcher.generalTab.priority.media="Medios"
AdvSceneSwitcher.generalTab.priority.time="Tiempo"
AdvSceneSwitcher.generalTab.priority.audio="Audio"
AdvSceneSwitcher.generalTab.priority.video="Video"
AdvSceneSwitcher.generalTab.priority.macro="Macro"
AdvSceneSwitcher.generalTab.setTransitionBy="Al cambiar las transiciones:"
AdvSceneSwitcher.generalTab.transitionOverride="Establecer anulaciones de transición"
@@ -61,38 +61,38 @@ AdvSceneSwitcher.generalTab.transitionBehaviorSelectionError="Al menos una opci
; Macro Tab
AdvSceneSwitcher.macroTab.title="Macro"
AdvSceneSwitcher.macroTab.macros="Macros"
AdvSceneSwitcher.macroTab.priorityWarning="Nota: Se recomienda configurar las macros para que sean la funcionalidad de máxima prioridad.\nEsta configuración se puede cambiar en la ficha General."
AdvSceneSwitcher.macroTab.help="Las macros le permiten ejecutar una cadena de acciones en función de varias condiciones.\n\nHaga clic en el símbolo más resaltado para agregar una nueva macro."
AdvSceneSwitcher.macroTab.editConditionHelp="Esta sección le permite definir las condiciones de la macro.\n\nSeleccione una macro existente o agregue una nueva a la izquierda.\nLuego haga clic en el botón más a continuación para agregar una nueva condición."
AdvSceneSwitcher.macroTab.priorityWarning="Nota: Se recomienda configurar las macros para que sean la funcionalidad de máxima prioridad.\nEsta configuración se puede cambiar en la ficha General."
AdvSceneSwitcher.macroTab.help="Las macros le permiten ejecutar una cadena de acciones en función de varias condiciones.\n\nHaga clic en el símbolo más resaltado para agregar una nueva macro."
AdvSceneSwitcher.macroTab.editConditionHelp="Esta sección le permite definir las condiciones de la macro.\n\nSeleccione una macro existente o agregue una nueva a la izquierda.\nLuego haga clic en el botón más a continuación para agregar una nueva condición."
AdvSceneSwitcher.macroTab.editActionHelp="Esta sección le permite definir acciones de la macro.\n\nSeleccione una macro existente o agregue una nueva a la izquierda.\nLuego haga clic en el botón más a continuación para agregar una nueva acción"
AdvSceneSwitcher.macroTab.edit="Editar macro"
AdvSceneSwitcher.macroTab.edit.logic="Tipo de lógica:"
AdvSceneSwitcher.macroTab.edit.condition="Tipo de condición:"
AdvSceneSwitcher.macroTab.edit.logic="Tipo de lógica:"
AdvSceneSwitcher.macroTab.edit.condition="Tipo de condición:"
AdvSceneSwitcher.macroTab.edit.action="Tipo de acción:"
AdvSceneSwitcher.macroTab.add="Agregar nueva macro"
AdvSceneSwitcher.macroTab.name="Nombre:"
AdvSceneSwitcher.macroTab.run="Ejecutar macro"
AdvSceneSwitcher.macroTab.runFail="Error al ejecutar \"%1\" si se ha producido un error en una de las acciones o si la macro ya se está ejecutando."
AdvSceneSwitcher.macroTab.runInParallel="Ejecutar macro en paralelo a otras macros"
AdvSceneSwitcher.macroTab.onChange="Realizar acciones solo en el cambio de condición"
AdvSceneSwitcher.macroTab.defaultname="Macro %1"
AdvSceneSwitcher.macroTab.exists="El nombre de la macro ya existe"
AdvSceneSwitcher.macroTab.copy="Crear copia"
AdvSceneSwitcher.macroTab.expandAll="Expandir todo"
AdvSceneSwitcher.macroTab.collapseAll="Contraer todo"
AdvSceneSwitcher.macroTab.maximize="Maximizar"
AdvSceneSwitcher.macroTab.add="Agregar nueva macro"
AdvSceneSwitcher.macroTab.name="Nombre:"
AdvSceneSwitcher.macroTab.run="Ejecutar macro"
AdvSceneSwitcher.macroTab.runFail="Error al ejecutar \"%1\" si se ha producido un error en una de las acciones o si la macro ya se está ejecutando."
AdvSceneSwitcher.macroTab.runInParallel="Ejecutar macro en paralelo a otras macros"
AdvSceneSwitcher.macroTab.onChange="Realizar acciones solo en el cambio de condición"
AdvSceneSwitcher.macroTab.defaultname="Macro %1"
AdvSceneSwitcher.macroTab.exists="El nombre de la macro ya existe"
AdvSceneSwitcher.macroTab.copy="Crear copia"
AdvSceneSwitcher.macroTab.expandAll="Expandir todo"
AdvSceneSwitcher.macroTab.collapseAll="Contraer todo"
AdvSceneSwitcher.macroTab.maximize="Maximizar"
AdvSceneSwitcher.macroTab.minimize="Minimizar"
AdvSceneSwitcher.macroTab.highlightExecutedMacros="Resaltar macros ejecutadas recientemente"
AdvSceneSwitcher.macroTab.highlightTrueConditions="Resaltar condiciones de la macro seleccionada, que se evaluaron como verdaderas recientemente"
AdvSceneSwitcher.macroTab.highlightPerformedActions="Resaltar acciones realizadas recientemente de la macro seleccionada actualmente"
AdvSceneSwitcher.macroTab.highlightExecutedMacros="Resaltar macros ejecutadas recientemente"
AdvSceneSwitcher.macroTab.highlightTrueConditions="Resaltar condiciones de la macro seleccionada, que se evaluaron como verdaderas recientemente"
AdvSceneSwitcher.macroTab.highlightPerformedActions="Resaltar acciones realizadas recientemente de la macro seleccionada actualmente"
; Lógica de macros
AdvSceneSwitcher.logic.none="Omitir entrada"
AdvSceneSwitcher.logic.and="Y"
AdvSceneSwitcher.logic.or="O"
AdvSceneSwitcher.logic.andNot="Y no"
AdvSceneSwitcher.logic.orNot="O no"
AdvSceneSwitcher.logic.rootNone="Si"
; Lógica de macros
AdvSceneSwitcher.logic.none="Omitir entrada"
AdvSceneSwitcher.logic.and="Y"
AdvSceneSwitcher.logic.or="O"
AdvSceneSwitcher.logic.andNot="Y no"
AdvSceneSwitcher.logic.orNot="O no"
AdvSceneSwitcher.logic.rootNone="Si"
AdvSceneSwitcher.logic.not="Si no"
; Macro Conditions
@@ -123,9 +123,6 @@ AdvSceneSwitcher.condition.scene.entry.line1="{{sceneType}}{{scenes}}{{pattern}}
AdvSceneSwitcher.condition.scene.entry.line2="{{useTransitionTargetScene}}"
AdvSceneSwitcher.condition.window="Ventana"
AdvSceneSwitcher.condition.file="Archivo"
AdvSceneSwitcher.condition.file.entry.line1="Contenido de{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Medios"
AdvSceneSwitcher.condition.media.anyOnScene="Cualquier fuente multimedia activada"
AdvSceneSwitcher.condition.media.allOnScene="Todas las fuentes de medios activadas"
@@ -182,9 +179,8 @@ AdvSceneSwitcher.condition.record="Grabación"
AdvSceneSwitcher.condition.record.state.start="Grabación en ejecución"
AdvSceneSwitcher.condition.record.state.pause="Grabación en pausa"
AdvSceneSwitcher.condition.record.state.stop="Grabación detenida"
AdvSceneSwitcher.condition.record.entry="{{recordState}}"
AdvSceneSwitcher.condition.process="Proceso"
AdvSceneSwitcher.condition.process.entry="{{processes}} se está ejecutando {{focused}} y está enfocado"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}se está ejecutando{{focused}}y está enfocado"
AdvSceneSwitcher.condition.idle="Inactivo"
AdvSceneSwitcher.condition.idle.entry="No hay entradas de teclado o ratón durante {{duration}}"
AdvSceneSwitcher.condition.pluginState="Estado del complemento"
@@ -275,7 +271,8 @@ AdvSceneSwitcher.condition.date.entry.nextMatchDate="Próxima coincidencia en: %
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}} Al repetir actualizar la fecha seleccionada para repetir la fecha"
AdvSceneSwitcher.condition.sceneTransform="Transformar elemento de escena"
AdvSceneSwitcher.condition.sceneTransform.getTransform="Obtener transformación"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="En{{scenes}}{{sources}}coincide con la transformación"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="En{{scenes}}{{sources}}{{types}}"
AdvSceneSwitcher.condition.sceneTransform.entry.type.matches="coincide con la transformación"
AdvSceneSwitcher.condition.sceneTransform.entry.line2="{{settings}}"
AdvSceneSwitcher.condition.sceneTransform.entry.line3="{{regex}} {{getSettings}}"
AdvSceneSwitcher.condition.transition="Transición"
@@ -386,8 +383,7 @@ AdvSceneSwitcher.action.media.type.stop="Detener"
AdvSceneSwitcher.action.media.type.restart="Reiniciar"
AdvSceneSwitcher.action.media.type.next="Siguiente"
AdvSceneSwitcher.action.media.type.previous="Anterior"
AdvSceneSwitcher.action.media.type.seek="Buscar"
AdvSceneSwitcher.action.media.entry="{{actions}}{{duration}}{{mediaSources}}"
AdvSceneSwitcher.action.media.type.seek.duration="Buscar"
AdvSceneSwitcher.action.macro="Macro"
AdvSceneSwitcher.action.macro.type.pause="Pausa"
AdvSceneSwitcher.action.macro.type.unpause="Reanudar"
@@ -566,7 +562,7 @@ AdvSceneSwitcher.fileTab.help="Esta pestaña te permitirá cambiar escenas autom
; Random Tab
AdvSceneSwitcher.randomTab.title="Aleatorio"
AdvSceneSwitcher.randomTab.randomDisabledWarning="Funcionalidad deshabilitada - para activarla, selecciona \"Si no se cumple ninguna condición de cambio, cambiar a cualquier escena en la pestaña Aleatorio\" en la pestaña General"
AdvSceneSwitcher.randomTab.entry="Si no se cumple ninguna condición de cambio, cambiar a {{scenes}} usando {{transitions}} después de {{delay}}"
AdvSceneSwitcher.randomTab.entry="Si no se cumple ninguna condición de cambio, cambiar a {{scenes}} usando {{transitions}} después de {{delay}}"
AdvSceneSwitcher.randomTab.help="El selector de escenas elegirá aleatoriamente una entrada en esta pestaña para cambiar durante el tiempo configurado.\nTen en cuenta que la misma entrada no se elegirá dos veces seguidas.\n\nHaz clic en el símbolo más resaltado para continuar."
; Time Tab

View File

@@ -286,7 +286,7 @@ AdvSceneSwitcher.condition.record.state.start="Enregistrement en cours"
AdvSceneSwitcher.condition.record.state.pause="Enregistrement en pause"
AdvSceneSwitcher.condition.record.state.stop="Arrêt de l'enregistrement"
AdvSceneSwitcher.condition.process="Processus"
AdvSceneSwitcher.condition.process.entry="{{processes}}en cours d'exécution{{focused}}et est au premier plan"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}en cours d'exécution{{focused}}et est au premier plan"
AdvSceneSwitcher.condition.process.entry.focus="Processus au premier plan actuel :{{focusProcess}}"
AdvSceneSwitcher.condition.idle="Inactif"
AdvSceneSwitcher.condition.idle.entry="Aucune entrée de clavier ou de souris pendant{{duration}}"
@@ -355,7 +355,7 @@ AdvSceneSwitcher.condition.hotkey="Raccourci clavier"
AdvSceneSwitcher.condition.hotkey.name="Raccourci clavier de déclenchement de macro"
AdvSceneSwitcher.condition.hotkey.tip="Remarque : Vous pouvez configurer les raccourcis clavier pour ce raccourci dans la fenêtre de paramètres d'OBS"
AdvSceneSwitcher.condition.hotkey.entry.line1="Raccourci clavier est enfoncé"
AdvSceneSwitcher.condition.hotkey.entry.line2="Nom : {{name}}"
AdvSceneSwitcher.condition.hotkey.entry.line2="Nom :{{name}}"
AdvSceneSwitcher.condition.replay="Tampon de répétition"
AdvSceneSwitcher.condition.replay.state.stopped="Tampon de répétition arrêté"
AdvSceneSwitcher.condition.replay.state.started="Tampon de répétition démarré"
@@ -386,7 +386,8 @@ AdvSceneSwitcher.condition.date.entry.nextMatchDate="Prochaine correspondance à
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}}À la répétition, mettre à jour la date sélectionnée pour la date de répétition"
AdvSceneSwitcher.condition.sceneTransform="Transformation de l'élément de la scène"
AdvSceneSwitcher.condition.sceneTransform.getTransform="Obtenir la transformation"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="Sur{{scenes}}{{sources}}correspond à la transformation"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="Sur{{scenes}}{{sources}}{{types}}"
AdvSceneSwitcher.condition.sceneTransform.entry.type.matches="correspond à la transformation"
AdvSceneSwitcher.condition.transition="Transition"
AdvSceneSwitcher.condition.transition.type.current="Le type de transition actuel est"
AdvSceneSwitcher.condition.transition.type.duration="La durée de la transition actuelle est"
@@ -426,7 +427,7 @@ AdvSceneSwitcher.condition.stats.condition.above="au-dessus de"
AdvSceneSwitcher.condition.stats.condition.equals="égal à"
AdvSceneSwitcher.condition.stats.condition.below="en dessous de"
AdvSceneSwitcher.condition.stats.dockHint="Vous pouvez ouvrir le dock \"Statistiques\" pour voir l'état actuel"
AdvSceneSwitcher.condition.stats.entry="{{stats}} est {{condition}} {{value}}"
AdvSceneSwitcher.condition.stats.entry="{{stats}}est{{condition}}{{value}}"
AdvSceneSwitcher.condition.profile="Profil"
AdvSceneSwitcher.condition.profile.entry="Le profil actif actuel est{{profiles}}"
AdvSceneSwitcher.condition.websocket="Websocket"
@@ -434,7 +435,7 @@ AdvSceneSwitcher.condition.websocket.type.request="Demande de commutation de sc
AdvSceneSwitcher.condition.websocket.type.event="Événement de commutation de scène"
AdvSceneSwitcher.condition.websocket.useRegex="Utiliser des expressions régulières"
AdvSceneSwitcher.condition.websocket.entry.request="{{type}}a été reçu :"
AdvSceneSwitcher.condition.websocket.entry.event="{{type}}a été reçu de{{connection}} :"
AdvSceneSwitcher.condition.websocket.entry.event="{{type}}a été reçu de{{connection}}:"
AdvSceneSwitcher.condition.variable="Variable"
AdvSceneSwitcher.condition.variable.type.compare="est égal à"
AdvSceneSwitcher.condition.variable.type.empty="est vide"
@@ -556,7 +557,7 @@ AdvSceneSwitcher.action.media.type.stop="Arrêt"
AdvSceneSwitcher.action.media.type.restart="Redémarrage"
AdvSceneSwitcher.action.media.type.next="Suivant"
AdvSceneSwitcher.action.media.type.previous="Précédent"
AdvSceneSwitcher.action.media.type.seek="Recherche vers"
AdvSceneSwitcher.action.media.type.seek.duration="Recherche vers"
AdvSceneSwitcher.action.macro="Macro"
AdvSceneSwitcher.action.macro.type.pause="Pause"
AdvSceneSwitcher.action.macro.type.unpause="Reprendre"
@@ -594,15 +595,15 @@ AdvSceneSwitcher.action.hotkey.leftMeta="Méta gauche"
AdvSceneSwitcher.action.hotkey.rightMeta="Touche Droite Meta"
AdvSceneSwitcher.action.hotkey.onlyOBS="Envoyer la pression de touche uniquement à OBS"
AdvSceneSwitcher.action.hotkey.disabled="Impossible de simuler des pressions de touche globales - la fonctionnalité est limitée à l'envoi de la pression de touche à OBS uniquement !"
AdvSceneSwitcher.action.hotkey.entry.custom="Appuyer sur {{actionType}} {{keys}} pendant {{duration}}"
AdvSceneSwitcher.action.hotkey.entry.obs="Appuyer sur {{actionType}} {{hotkeyType}} {{obsHotkeys}}"
AdvSceneSwitcher.action.hotkey.entry.custom="Appuyer sur{{actionType}}{{keys}}pendant{{duration}}"
AdvSceneSwitcher.action.hotkey.entry.obs="Appuyer sur{{actionType}}{{hotkeyType}}{{obsHotkeys}}"
AdvSceneSwitcher.action.sceneOrder="Ordre des éléments de la scène"
AdvSceneSwitcher.action.sceneOrder.type.moveUp="Déplacer vers le haut"
AdvSceneSwitcher.action.sceneOrder.type.moveDown="Déplacer vers le bas"
AdvSceneSwitcher.action.sceneOrder.type.moveTop="Déplacer en haut"
AdvSceneSwitcher.action.sceneOrder.type.moveBottom="Déplacer en bas"
AdvSceneSwitcher.action.sceneOrder.type.movePosition="Déplacer à la position"
AdvSceneSwitcher.action.sceneOrder.entry="Sur {{scenes}} {{actions}} {{sources}} {{position}}"
AdvSceneSwitcher.action.sceneOrder.entry="Sur{{scenes}}{{actions}}{{sources}}{{position}}"
AdvSceneSwitcher.action.sceneTransform="Transformation de l'élément de la scène"
AdvSceneSwitcher.action.sceneTransform.type.manual="Transform"
AdvSceneSwitcher.action.sceneTransform.type.reset="Réinitialiser la transformation"
@@ -615,11 +616,11 @@ AdvSceneSwitcher.action.sceneTransform.type.centerToScreen="Centrer à l'écran"
AdvSceneSwitcher.action.sceneTransform.type.centerVertically="Centrer verticalement"
AdvSceneSwitcher.action.sceneTransform.type.centerHorizontally="Centrer horizontalement"
AdvSceneSwitcher.action.sceneTransform.getTransform="Obtenir la transformation"
AdvSceneSwitcher.action.sceneTransform.entry="Sur {{scenes}} {{action}} {{rotation}} {{sources}}"
AdvSceneSwitcher.action.sceneTransform.entry="Sur{{scenes}}{{action}}{{rotation}}{{sources}}"
AdvSceneSwitcher.action.file="Fichier"
AdvSceneSwitcher.action.file.type.write="Écrire"
AdvSceneSwitcher.action.file.type.append="Ajouter"
AdvSceneSwitcher.action.file.entry="{{actions}} vers {{filePath}} :"
AdvSceneSwitcher.action.file.entry="{{actions}}vers{{filePath}}:"
AdvSceneSwitcher.action.studioMode="Mode studio"
AdvSceneSwitcher.action.studioMode.type.swap="Permuter la scène de prévisualisation et la scène du programme"
AdvSceneSwitcher.action.studioMode.type.setScene="Définir la scène de prévisualisation sur"
@@ -630,15 +631,15 @@ AdvSceneSwitcher.action.transition.type.scene="Transition de scène"
AdvSceneSwitcher.action.transition.type.sceneOverride="Remplacement de la transition de scène"
AdvSceneSwitcher.action.transition.type.sourceShow="Transition d'affichage de la source"
AdvSceneSwitcher.action.transition.type.sourceHide="Transition de masquage de la source"
AdvSceneSwitcher.action.transition.entry.line1="Modifier {{type}} {{scenes}} {{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}} Définir le type de transition sur {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}} Définir la durée de la transition à {{duration}} secondes"
AdvSceneSwitcher.action.transition.entry.line1="Modifier{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Définir le type de transition sur{{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Définir la durée de la transition à{{duration}}secondes"
AdvSceneSwitcher.action.timer="Minuterie"
AdvSceneSwitcher.action.timer.type.pause="Mettre en pause"
AdvSceneSwitcher.action.timer.type.continue="Continuer"
AdvSceneSwitcher.action.timer.type.reset="Réinitialiser"
AdvSceneSwitcher.action.timer.type.setTimeRemaining="Définir le temps restant de"
AdvSceneSwitcher.action.timer.entry="{{timerAction}} minuteries sur {{macros}} {{duration}}"
AdvSceneSwitcher.action.timer.entry="{{timerAction}}minuteries sur{{macros}}{{duration}}"
AdvSceneSwitcher.action.random="Aléatoire"
AdvSceneSwitcher.action.random.allowRepeat="Autoriser l'exécution consécutive du même macro"
AdvSceneSwitcher.action.random.entry="Exécuter au hasard l'un des macros suivants (les macros en pause sont ignorées)"
@@ -654,11 +655,11 @@ 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}}"
AdvSceneSwitcher.action.screenshot.entry="Capturer{{targetType}}{{sources}}{{scenes}}et enregistrer à l'emplacement{{saveType}}"
AdvSceneSwitcher.action.profile="Profil"
AdvSceneSwitcher.action.profile.entry="Changer le profil actif vers {{profiles}}"
AdvSceneSwitcher.action.profile.entry="Changer le profil actif vers{{profiles}}"
AdvSceneSwitcher.action.sceneCollection="Collection de scènes"
AdvSceneSwitcher.action.sceneCollection.entry="Changer la collection de scènes active vers {{sceneCollections}}"
AdvSceneSwitcher.action.sceneCollection.entry="Changer la collection de scènes active vers{{sceneCollections}}"
AdvSceneSwitcher.action.sceneCollection.warning="Note : Toutes les actions suivantes ne seront pas exécutées car le changement de collection de scènes rechargera également les paramètres du commutateur de scènes. L'action de collection de scènes sera ignorée lorsque la fenêtre des paramètres est ouverte."
AdvSceneSwitcher.action.sequence="Séquence"
AdvSceneSwitcher.action.sequence.entry="Chaque fois que cette action est effectuée, exécutez le macro suivant dans la liste (les macros en pause sont ignorés)"
@@ -674,17 +675,17 @@ AdvSceneSwitcher.action.websocket.type.request="Requête"
AdvSceneSwitcher.action.websocket.type.event="Événement"
AdvSceneSwitcher.action.websocket.settingsConflictGeneric="Possible conflit de paramètres : Sélection de message websocket générique avec une connexion qui suit le protocole OBS !"
AdvSceneSwitcher.action.websocket.settingsConflictOBS="Possible conflit de paramètres : Connexion sélectionnée qui ne prend en charge que des messages websocket génériques, alors que le type de message nécessite le protocole OBS !"
AdvSceneSwitcher.action.websocket.entry.sceneSwitcher.request="Envoyer {{api}} de type {{type}} via {{connection}}"
AdvSceneSwitcher.action.websocket.entry.sceneSwitcher.event="Envoyer {{api}} de type {{type}} aux clients connectés"
AdvSceneSwitcher.action.websocket.entry.generic="Envoyer {{api}} via {{connection}}"
AdvSceneSwitcher.action.websocket.entry.sceneSwitcher.request="Envoyer{{api}}de type{{type}}via{{connection}}"
AdvSceneSwitcher.action.websocket.entry.sceneSwitcher.event="Envoyer{{api}}de type{{type}}aux clients connectés"
AdvSceneSwitcher.action.websocket.entry.generic="Envoyer{{api}}via{{connection}}"
AdvSceneSwitcher.action.http="HTTP"
AdvSceneSwitcher.action.http.setHeaders="Définir les en-têtes"
AdvSceneSwitcher.action.http.headers="En-têtes :"
AdvSceneSwitcher.action.http.addHeader="Ajouter un en-tête"
AdvSceneSwitcher.action.http.type.get="GET"
AdvSceneSwitcher.action.http.type.post="POST"
AdvSceneSwitcher.action.http.entry.line1="Envoyer {{method}} vers {{url}}"
AdvSceneSwitcher.action.http.entry.line2="Délai d'attente : {{timeout}} secondes"
AdvSceneSwitcher.action.http.entry.line1="Envoyer{{method}}vers{{url}}"
AdvSceneSwitcher.action.http.entry.line2="Délai d'attente :{{timeout}}secondes"
AdvSceneSwitcher.action.variable="Variable"
AdvSceneSwitcher.action.variable.type.set="Définir sur une valeur fixe"
AdvSceneSwitcher.action.variable.type.append="Ajouter"
@@ -710,8 +711,8 @@ AdvSceneSwitcher.action.variable.invalidSelection="Sélection invalide !"
AdvSceneSwitcher.action.variable.actionNoVariableSupport="La récupération de valeurs de variables à partir d'actions %1 n'est pas prise en charge !"
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="La récupération de valeurs de variables à partir de conditions %1 n'est pas prise en charge !"
AdvSceneSwitcher.action.variable.currentSegmentValue="Valeur actuelle :"
AdvSceneSwitcher.action.variable.entry.substringIndex="Début de la sous-chaîne : {{subStringStart}} Taille de la sous-chaîne : {{subStringSize}}"
AdvSceneSwitcher.action.variable.entry.substringRegex="Attribuer la valeur du match {{regexMatchIdx}} en utilisant une expression régulière :"
AdvSceneSwitcher.action.variable.entry.substringIndex="Début de la sous-chaîne :{{subStringStart}}Taille de la sous-chaîne :{{subStringSize}}"
AdvSceneSwitcher.action.variable.entry.substringRegex="Attribuer la valeur du match{{regexMatchIdx}}en utilisant une expression régulière :"
AdvSceneSwitcher.action.variable.entry.userInput.customPrompt="{{useCustomPrompt}}Utiliser un message personnalisé{{inputPrompt}}"
AdvSceneSwitcher.action.variable.entry.userInput.placeholder="{{useInputPlaceholder}}Remplir avec un indicateur de position{{inputPlaceholder}}"
AdvSceneSwitcher.action.projector="Projecteur"
@@ -723,39 +724,33 @@ 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.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}}"
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}}"
AdvSceneSwitcher.action.osc="Open Sound Control"
AdvSceneSwitcher.action.sceneLock="Verrouillage de l'élément de scène"
AdvSceneSwitcher.action.sceneLock.type.lock="verrouiller"
AdvSceneSwitcher.action.sceneLock.type.unlock="déverrouiller"
AdvSceneSwitcher.action.sceneLock.type.toggle="basculer le verrouillage de"
AdvSceneSwitcher.action.sceneLock.entry="Sur {{scenes}} {{actions}} {{sources}}"
AdvSceneSwitcher.action.sceneLock.entry="Sur{{scenes}}{{actions}}{{sources}}"
AdvSceneSwitcher.action.twitch="Twitch"
AdvSceneSwitcher.action.twitch.type.title="Définir le titre du flux"
AdvSceneSwitcher.action.twitch.type.category="Définir la catégorie du flux"
AdvSceneSwitcher.action.twitch.type.marker="Create stream marker"
AdvSceneSwitcher.action.twitch.type.clip="Create stream clip"
AdvSceneSwitcher.action.twitch.type.commercial="Démarrer une publicité d'une durée de"
AdvSceneSwitcher.action.twitch.type.channel.info.title.set="Définir le titre du flux"
AdvSceneSwitcher.action.twitch.type.channel.info.category.set="Définir la catégorie du flux"
AdvSceneSwitcher.action.twitch.type.commercial.start="Démarrer une publicité d'une durée de"
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Impossible de sélectionner une catégorie sans avoir d'abord sélectionné un compte Twitch !"
AdvSceneSwitcher.action.twitch.entry="Sur {{account}} {{actions}} {{streamTitle}} {{category}} {{manualCategorySearch}} {{markerDescription}} {{clipHasDelay}} {{duration}}"
AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient="Permissions of selected token are insufficient to perform selected action!"
AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip"
AdvSceneSwitcher.action.twitch.marker.description="Describe marker"
AdvSceneSwitcher.action.twitch.title.title="Enter title"
AdvSceneSwitcher.action.twitch.entry.line1="Sur{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}"
; Transition Tab
AdvSceneSwitcher.transitionTab.title="Transition"
AdvSceneSwitcher.transitionTab.transitionForAToB="Utiliser une transition pour le passage automatisé de la scène A à la scène B"
AdvSceneSwitcher.transitionTab.transitionsHelp="<html><head/><body><p>Ces paramètres n'affectent <span style=\"font-style:italic;\">que</span> les transitions provoquées par le commutateur de scènes - Consultez la <a href=\"https://obsproject.com/forum/resources/transition-table.1174/\"><span style=\" text-decoration: underline; color:#268bd2;\">Table de transitions</span></a> si vous souhaitez les configurer pour des changements de scène manuels.<br/>Les paramètres définis ici ont la priorité sur les paramètres de transition configurés ailleurs dans le commutateur de scènes.<br/><br/>Cliquez sur le symbole plus ci-dessous pour ajouter une nouvelle entrée.</p></body></html>"
AdvSceneSwitcher.transitionTab.defaultTransition="Changer la transition si la scène est active"
AdvSceneSwitcher.transitionTab.entry="Passer de {{scenes}} à {{scenes2}} en utilisant {{transitions}} avec une durée de {{duration}}"
AdvSceneSwitcher.transitionTab.defaultTransitionEntry="Lorsque la scène {{scenes}} est active, changer la transition de scène par défaut en {{transitions}}"
AdvSceneSwitcher.transitionTab.entry="Passer de{{scenes}}à{{scenes2}}en utilisant{{transitions}}avec une durée de{{duration}}"
AdvSceneSwitcher.transitionTab.defaultTransitionEntry="Lorsque la scène{{scenes}}est active, changer la transition de scène par défaut en{{transitions}}"
AdvSceneSwitcher.transitionTab.defaultTransitionsHelp="Cliquez sur le symbole plus pour ajouter une entrée."
AdvSceneSwitcher.transitionTab.defaultTransition.delay="Changer la transition après le délai de {{defTransitionDelay}} après le changement de scène."
AdvSceneSwitcher.transitionTab.defaultTransition.delay="Changer la transition après le délai de{{defTransitionDelay}}après le changement de scène."
AdvSceneSwitcher.transitionTab.defaultTransition.delay.help="Le délai est utilisé pour éviter les changements de scène annulés, qui peuvent se produire si le type de transition est modifié alors qu'une transition est encore en cours."
; Pause Scenes Tab
@@ -766,7 +761,7 @@ AdvSceneSwitcher.pauseTab.pauseInFocus2="est au premier plan"
AdvSceneSwitcher.pauseTab.pauseTypeScene="la scène est active"
AdvSceneSwitcher.pauseTab.pauseTypeWindow="la fenêtre est au premier plan"
AdvSceneSwitcher.pauseTab.pauseTargetAll="tous"
AdvSceneSwitcher.pauseTab.pauseEntry="Mettre en pause {{pauseTargets}}vérifications lorsque {{pauseTypes}}{{scenes}}{{windows}}"
AdvSceneSwitcher.pauseTab.pauseEntry="Mettre en pause{{pauseTargets}}vérifications lorsque{{pauseTypes}}{{scenes}}{{windows}}"
AdvSceneSwitcher.pauseTab.help="Sur cet onglet, vous pouvez configurer la pause des méthodes de commutation individuelles si une scène est active ou si une fenêtre est au premier plan.\n\nCliquez sur le symbole plus en surbrillance pour continuer."
; Window Title Tab
@@ -784,7 +779,7 @@ AdvSceneSwitcher.windowTitleTab.ignoreWindowsHelp="Si un titre de fenêtre est i
AdvSceneSwitcher.executableTab.title="Exécutable"
AdvSceneSwitcher.executableTab.implemented="Implémenté par dasOven"
AdvSceneSwitcher.executableTab.requiresFocus="uniquement si au premier plan"
AdvSceneSwitcher.executableTab.entry="Lorsque {{processes}} est en cours d'exécution, basculer vers {{scenes}} en utilisant {{transitions}} {{requiresFocus}}"
AdvSceneSwitcher.executableTab.entry="Lorsque{{processes}}est en cours d'exécution, basculer vers{{scenes}}en utilisant{{transitions}}{{requiresFocus}}"
AdvSceneSwitcher.executableTab.help="Cet onglet vous permettra de basculer automatiquement entre les scènes si un processus est en cours d'exécution.\nCela peut être utile dans des situations où le nom de la fenêtre pourrait changer ou n'est pas connu.\n\nCliquez sur le symbole plus en surbrillance pour continuer."
; Screen Region Tab
@@ -937,7 +932,7 @@ AdvSceneSwitcher.sceneGroupTab.title="Groupe de Scènes"
AdvSceneSwitcher.sceneGroupTab.list="Groupes de Scènes"
AdvSceneSwitcher.sceneGroupTab.edit="Modifier les Groupes de Scènes"
AdvSceneSwitcher.sceneGroupTab.edit.name="Nom :"
AdvSceneSwitcher.sceneGroupTab.edit.type="Type : {{type}}"
AdvSceneSwitcher.sceneGroupTab.edit.type="Type :{{type}}"
AdvSceneSwitcher.sceneGroupTab.type.count="Compteur"
AdvSceneSwitcher.sceneGroupTab.type.time="Temps"
AdvSceneSwitcher.sceneGroupTab.type.random="Aléatoire"
@@ -1122,12 +1117,9 @@ AdvSceneSwitcher.twitchToken.request.success="Jeton obtenu avec succès !"
AdvSceneSwitcher.twitchToken.request.success.browser="Authentification réussie ! Vous pouvez maintenant fermer cette fenêtre."
AdvSceneSwitcher.twitchToken.request.notSet="Aucun jeton défini - Veuillez demander un nouveau jeton !"
AdvSceneSwitcher.twitchToken.permissions="Permissions du jeton :"
AdvSceneSwitcher.twitchToken.analytics.readExtensions="Afficher les données d'analyse pour les extensions Twitch détenues par le compte authentifié."
AdvSceneSwitcher.twitchToken.analytics.readGames="Afficher les données d'analyse pour les jeux détenus par le compte authentifié."
AdvSceneSwitcher.twitchToken.bits.read="Afficher les informations sur les Bits pour une chaîne."
AdvSceneSwitcher.twitchToken.channel.manageBroadcast="Gérer la configuration de diffusion d'une chaîne, y compris la mise à jour de la configuration de la chaîne et la gestion des marqueurs de diffusion en continu et des balises de diffusion en continu."
AdvSceneSwitcher.twitchToken.channel.startCommercial="Lancer des publicités sur une chaîne."
AdvSceneSwitcher.twitchToken.channel.createClip="Create clips from channel's broadcasts."
AdvSceneSwitcher.twitchToken.channel.broadcast.manage="Gérer la configuration de diffusion d'une chaîne, y compris la mise à jour de la configuration de la chaîne et la gestion des marqueurs de diffusion en continu et des balises de diffusion en continu."
AdvSceneSwitcher.twitchToken.channel.commercial.edit="Lancer des publicités sur une chaîne."
AdvSceneSwitcher.twitchCategories.fetchStart="Récupération des catégories de streams..."
AdvSceneSwitcher.twitchCategories.fetchStatus="Obtenu %1 catégories de streams."

View File

@@ -87,8 +87,6 @@ AdvSceneSwitcher.condition.scene.type.current="Текущий"
AdvSceneSwitcher.condition.scene.type.previous="Предыдущий"
AdvSceneSwitcher.condition.window="Окно"
AdvSceneSwitcher.condition.file="Файл"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Медиа"
AdvSceneSwitcher.condition.video="Видео"
AdvSceneSwitcher.condition.video.condition.match="точно соответствует"
@@ -107,7 +105,7 @@ AdvSceneSwitcher.condition.record.state.start="Запись запущена"
AdvSceneSwitcher.condition.record.state.pause="Запись приостановлена"
AdvSceneSwitcher.condition.record.state.stop="Запись остановлена"
AdvSceneSwitcher.condition.process="Процесс"
AdvSceneSwitcher.condition.process.entry="{{processes}} запущен {{focused}} и сфокусирован"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}запущен{{focused}}и сфокусирован"
AdvSceneSwitcher.condition.idle="Простой"
AdvSceneSwitcher.condition.idle.entry="Не было ни клавиатуры, ни мыши в течении{{duration}}"
AdvSceneSwitcher.condition.pluginState="Состояние плагина"

View File

@@ -113,9 +113,6 @@ AdvSceneSwitcher.condition.scene.entry.line1="{{sceneType}}{{scenes}}{{pattern}}
AdvSceneSwitcher.condition.scene.entry.line2="{{useTransitionTargetScene}}"
AdvSceneSwitcher.condition.window="Pencere"
AdvSceneSwitcher.condition.file="Dosya"
AdvSceneSwitcher.condition.file.entry.line1="İçerik{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Medya"
AdvSceneSwitcher.condition.media.anyOnScene="Herhangi bir medya kaynağı"
AdvSceneSwitcher.condition.media.allOnScene="Tüm medya kaynakları "
@@ -159,9 +156,8 @@ AdvSceneSwitcher.condition.record="Kayıt"
AdvSceneSwitcher.condition.record.state.start="Kayıt Çalışıyor"
AdvSceneSwitcher.condition.record.state.pause="Kayıt durakladı"
AdvSceneSwitcher.condition.record.state.stop="Kayıt durdu"
AdvSceneSwitcher.condition.record.entry="{{recordState}}"
AdvSceneSwitcher.condition.process="İşlem"
AdvSceneSwitcher.condition.process.entry="{{processes}} çalışıyor {{focused}} ve odaklandı"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}çalışıyor{{focused}}ve odaklandı"
AdvSceneSwitcher.condition.idle="Boşta"
AdvSceneSwitcher.condition.idle.entry="...için klavye veya fare girişi yok {{duration}}"
AdvSceneSwitcher.condition.pluginState="Eklenti durumu"
@@ -231,7 +227,8 @@ AdvSceneSwitcher.condition.date.state.before="Önce"
AdvSceneSwitcher.condition.date.state.between="Arasında"
AdvSceneSwitcher.condition.sceneTransform="Sahne öğesi dönüşümü"
AdvSceneSwitcher.condition.sceneTransform.getTransform="Dönüşümü al"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="Açık{{scenes}}{{sources}} dönüşümle eşleşir"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="Açık{{scenes}}{{sources}}{{types}}"
AdvSceneSwitcher.condition.sceneTransform.entry.type.matches="dönüşümle eşleşir"
AdvSceneSwitcher.condition.sceneTransform.entry.line2="{{settings}}"
AdvSceneSwitcher.condition.sceneTransform.entry.line3="{{regex}} {{getSettings}}"
AdvSceneSwitcher.condition.transition="Geçiş"
@@ -316,7 +313,6 @@ AdvSceneSwitcher.action.media.type.stop="Dur"
AdvSceneSwitcher.action.media.type.restart="Yeniden Başlat"
AdvSceneSwitcher.action.media.type.next="Sonraki"
AdvSceneSwitcher.action.media.type.previous="Önceki"
AdvSceneSwitcher.action.media.entry="{{actions}}{{duration}}{{mediaSources}}"
AdvSceneSwitcher.action.macro="Makro"
AdvSceneSwitcher.action.macro.type.pause="Duraklat"
AdvSceneSwitcher.action.macro.type.unpause="Duraklatma"

View File

@@ -178,9 +178,6 @@ AdvSceneSwitcher.condition.file.type.contentChange="内容已更改"
AdvSceneSwitcher.condition.file.type.dateChange="修改日期已更改"
AdvSceneSwitcher.condition.file.remote="远程文件"
AdvSceneSwitcher.condition.file.local="本地文件"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="媒体"
AdvSceneSwitcher.condition.media.source="源"
AdvSceneSwitcher.condition.media.anyOnScene="任何媒体源"
@@ -267,9 +264,8 @@ AdvSceneSwitcher.condition.record="录制"
AdvSceneSwitcher.condition.record.state.start="录制运行中"
AdvSceneSwitcher.condition.record.state.pause="录制暂停"
AdvSceneSwitcher.condition.record.state.stop="录制停止"
AdvSceneSwitcher.condition.record.entry="{{recordState}}"
AdvSceneSwitcher.condition.process="进程"
AdvSceneSwitcher.condition.process.entry="{{processes}} 是正在运行 {{focused}} 并且是获得焦点"
AdvSceneSwitcher.condition.process.entry="{{processes}}{{regex}}是正在运行{{focused}}并且是获得焦点"
AdvSceneSwitcher.condition.process.entry.focus="当前焦点进程: {{focusProcess}}"
AdvSceneSwitcher.condition.idle="闲置检测"
AdvSceneSwitcher.condition.idle.entry="没有键盘或鼠标输入{{duration}}"
@@ -371,7 +367,8 @@ AdvSceneSwitcher.condition.date.entry.nextMatchDate="下一次匹配 在: %1"
AdvSceneSwitcher.condition.date.entry.updateOnRepeat="{{updateOnRepeat}} 重复时,将选定的日期更新为重复日期"
AdvSceneSwitcher.condition.sceneTransform="场景项目被改变"
AdvSceneSwitcher.condition.sceneTransform.getTransform="内容或者设置被改变了"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="在 {{scenes}} {{sources}} 匹配变换"
AdvSceneSwitcher.condition.sceneTransform.entry.line1="在 {{scenes}} {{sources}} {{types}}"
AdvSceneSwitcher.condition.sceneTransform.entry.type.matches="匹配变换"
AdvSceneSwitcher.condition.sceneTransform.entry.line2="{{settings}}"
AdvSceneSwitcher.condition.sceneTransform.entry.line3="{{regex}} {{getSettings}}"
AdvSceneSwitcher.condition.transition="转场"
@@ -435,7 +432,6 @@ AdvSceneSwitcher.condition.variable.type.valueChanged="值已更改"
AdvSceneSwitcher.condition.variable.type.equalsVariable="等于变量"
AdvSceneSwitcher.condition.variable.type.lessThanVariable="小于变量"
AdvSceneSwitcher.condition.variable.type.greaterThanVariable="大于变量"
AdvSceneSwitcher.condition.variable.entry="{{variables}}{{conditions}}{{strValue}}{{numValue}}{{variables2}}"
AdvSceneSwitcher.condition.run="运行"
AdvSceneSwitcher.condition.run.entry="进程在超时 {{timeout}} 秒之前退出"
AdvSceneSwitcher.condition.run.entry.exit="{{checkExitCode}}检查退出代码{{exitCode}}"
@@ -527,8 +523,7 @@ AdvSceneSwitcher.action.media.type.stop="停止"
AdvSceneSwitcher.action.media.type.restart="重新启动"
AdvSceneSwitcher.action.media.type.next="下一个"
AdvSceneSwitcher.action.media.type.previous="上一个"
AdvSceneSwitcher.action.media.type.seek="时间轴定位"
AdvSceneSwitcher.action.media.entry="{{actions}}{{duration}}{{mediaSources}}"
AdvSceneSwitcher.action.media.type.seek.duration="时间轴定位"
AdvSceneSwitcher.action.macro="宏"
AdvSceneSwitcher.action.macro.type.pause="暂停"
AdvSceneSwitcher.action.macro.type.unpause="取消暂停"
@@ -652,7 +647,7 @@ AdvSceneSwitcher.action.variable.invalidSelection="无效选择!"
AdvSceneSwitcher.action.variable.actionNoVariableSupport="不支持从 %1 个操作获取变量值!"
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="不支持从 %1 条件中获取变量值!"
AdvSceneSwitcher.action.variable.currentSegmentValue="当前值:"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}{{tempVars}}"
AdvSceneSwitcher.action.variable.entry.substringIndex="子字符串开始:{{subStringStart}} 子字符串大小:{{subStringSize}}"
AdvSceneSwitcher.action.variable.entry.substringRegex="使用正则表达式为 {{regexMatchIdx}} 匹配的值:"
AdvSceneSwitcher.action.variable.entry.findAndReplace="{{findStr}}{{replaceStr}}"

View File

@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" version="1.1">
<!-- Created with SVG-edit - https://github.com/SVG-Edit/svgedit-->
<g class="layer">
<title>Layer 1</title>
<rect fill="#fefefe" height="2.25" id="svg_1" stroke="#fefefe" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" transform="matrix(1, 0, 0, 1, 0, 0)" width="13" x="1.5" y="4"/>
<rect fill="#fefefe" height="2.25" id="svg_2" stroke="#fefefe" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" transform="matrix(1, 0, 0, 1, 0, 0)" width="13" x="1.5" y="10"/>
<rect fill="#fefefe" height="1" id="svg_4" stroke="#fefefe" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" transform="matrix(0.341951, -0.926734, 0.939718, 0.337226, -1.948, 13.2448)" width="14" x="1" y="7"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 931 B

View File

@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" version="1.1">
<!-- Created with SVG-edit - https://github.com/SVG-Edit/svgedit-->
<g class="layer">
<title>Layer 1</title>
<rect fill="#202020" height="2.25" id="svg_1" stroke="#202020" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" transform="matrix(1, 0, 0, 1, 0, 0)" width="13" x="1.5" y="4"/>
<rect fill="#202020" height="2.25" id="svg_2" stroke="#202020" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" transform="matrix(1, 0, 0, 1, 0, 0)" width="13" x="1.5" y="10"/>
<rect fill="#202020" height="1" id="svg_4" stroke="#202020" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" transform="matrix(0.341951, -0.926734, 0.939718, 0.337226, -1.948, 13.2448)" width="14" x="1" y="7"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 931 B

2
deps/asio vendored

Submodule deps/asio updated: b73dc1d2c0...c465349fa5

1
deps/json vendored Submodule

Submodule deps/json added at bc889afb4c

View File

@@ -1275,6 +1275,22 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="toggleElseActions">
<property name="maximumSize">
<size>
<width>22</width>
<height>16777215</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View File

@@ -115,6 +115,23 @@ void AdvSceneSwitcher::LoadUI()
loading = false;
}
bool AdvSceneSwitcher::eventFilter(QObject *obj, QEvent *event)
{
auto eventType = event->type();
if (obj == ui->macroElseActions && eventType == QEvent::Resize) {
QResizeEvent *resizeEvent = static_cast<QResizeEvent *>(event);
if (resizeEvent->size().height() == 0) {
SetElseActionsStateToHidden();
return QDialog::eventFilter(obj, event);
}
SetElseActionsStateToVisible();
}
return QDialog::eventFilter(obj, event);
}
/******************************************************************************
* Saving and loading
******************************************************************************/
@@ -325,14 +342,13 @@ void SwitcherData::SetPreconditions()
cursorPosChanged = cursorPos.first != switcher->lastCursorPos.first ||
cursorPos.second != switcher->lastCursorPos.second;
lastCursorPos = GetCursorPos();
}
void ClearWebsocketMessages();
// Macro
InvalidateMacroTempVarValues();
}
void SwitcherData::ResetForNextInterval()
{
// Core reset functions
ClearWebsocketMessages();
// Plugin reset functions
for (const auto &func : resetIntervalSteps) {
func();
@@ -756,6 +772,9 @@ void OpenSettingsWindow()
}
}
void SetupConnectionManager();
void SetupWebsocketHelpers();
extern "C" void InitSceneSwitcher(obs_module_t *module, translateFunc translate)
{
blog(LOG_INFO, "version: %s", g_GIT_TAG);
@@ -766,6 +785,8 @@ extern "C" void InitSceneSwitcher(obs_module_t *module, translateFunc translate)
PlatformInit();
LoadPlugins();
SetupDock();
SetupConnectionManager();
SetupWebsocketHelpers();
obs_frontend_add_save_callback(SaveSceneSwitcher, nullptr);
obs_frontend_add_event_callback(OBSEvent, switcher);

View File

@@ -39,6 +39,9 @@ public:
void RestoreWindowGeo();
void CheckFirstTimeSetup();
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
/* --- Begin of general tab section --- */
public:
void SetupGeneralTab();
@@ -115,6 +118,7 @@ public slots:
void on_actionUp_clicked();
void on_actionDown_clicked();
void on_actionBottom_clicked();
void on_toggleElseActions_clicked();
void on_elseActionAdd_clicked();
void on_elseActionRemove_clicked();
void on_elseActionTop_clicked();
@@ -146,6 +150,8 @@ public slots:
void MaximizeElseActions();
void MinimizeConditions();
void MaximizeConditions();
void SetElseActionsStateToHidden();
void SetElseActionsStateToVisible();
void MacroActionSelectionChanged(int idx);
void MacroActionReorder(int to, int target);
void AddMacroAction(int idx);
@@ -178,6 +184,7 @@ signals:
void MacroRemoved(const QString &name);
void MacroRenamed(const QString &oldName, const QString &newName);
void MacroSegmentOrderChanged();
void SegmentTempVarsChanged();
void HighlightMacrosChanged(bool value);
void HighlightActionsChanged(bool value);
void HighlightElseActionsChanged(bool value);
@@ -333,7 +340,7 @@ public slots:
void on_timeUp_clicked();
void on_timeDown_clicked();
// Video tab
// Audio tab
public:
void SetupAudioTab();
public slots:

View File

@@ -33,7 +33,7 @@ void AdvSceneSwitcher::on_noMatchDontSwitch_clicked()
}
std::lock_guard<std::mutex> lock(switcher->m);
switcher->switchIfNotMatching = SwitcherData::NoMatch::NO_SWITCH;
switcher->switchIfNotMatching = NoMatchBehavior::NO_SWITCH;
ui->noMatchSwitchScene->setEnabled(false);
ui->randomDisabledWarning->setVisible(true);
}
@@ -45,7 +45,7 @@ void AdvSceneSwitcher::on_noMatchSwitch_clicked()
}
std::lock_guard<std::mutex> lock(switcher->m);
switcher->switchIfNotMatching = SwitcherData::NoMatch::SWITCH;
switcher->switchIfNotMatching = NoMatchBehavior::SWITCH;
ui->noMatchSwitchScene->setEnabled(true);
UpdateNonMatchingScene(ui->noMatchSwitchScene->currentText());
ui->randomDisabledWarning->setVisible(true);
@@ -58,7 +58,7 @@ void AdvSceneSwitcher::on_noMatchRandomSwitch_clicked()
}
std::lock_guard<std::mutex> lock(switcher->m);
switcher->switchIfNotMatching = SwitcherData::NoMatch::RANDOM_SWITCH;
switcher->switchIfNotMatching = NoMatchBehavior::RANDOM_SWITCH;
ui->noMatchSwitchScene->setEnabled(false);
ui->randomDisabledWarning->setVisible(false);
}
@@ -499,11 +499,13 @@ void SwitcherData::LoadSettings(obs_data_t *obj)
return;
}
// New post load steps to be declared during load
postLoadSteps.clear();
// Needs to be loaded before any entries which might rely on scene group
// selections to be available.
loadSceneGroups(obj);
LoadVariables(obj);
LoadConnections(obj);
for (const auto &func : loadSteps) {
func(obj);
@@ -529,6 +531,10 @@ void SwitcherData::LoadSettings(obs_data_t *obj)
LoadHotkeys(obj);
LoadUISettings(obj);
for (const auto &func : postLoadSteps) {
func();
}
// Reset on startup and scene collection change
switcher->lastOpenedTab = -1;
startupLoadDone = true;
@@ -542,7 +548,6 @@ void SwitcherData::SaveSettings(obs_data_t *obj)
saveSceneGroups(obj);
SaveMacros(obj);
SaveConnections(obj);
SaveVariables(obj);
saveWindowTitleSwitches(obj);
saveScreenRegionSwitches(obj);
@@ -617,9 +622,9 @@ void SwitcherData::LoadGeneralSettings(obs_data_t *obj)
interval = obs_data_get_int(obj, "interval");
obs_data_set_default_int(obj, "switch_if_not_matching",
static_cast<int>(NoMatch::NO_SWITCH));
switchIfNotMatching =
(NoMatch)obs_data_get_int(obj, "switch_if_not_matching");
static_cast<int>(NoMatchBehavior::NO_SWITCH));
switchIfNotMatching = static_cast<NoMatchBehavior>(
obs_data_get_int(obj, "switch_if_not_matching"));
std::string nonMatchingSceneName =
obs_data_get_string(obj, "non_matching_scene");
nonMatchingScene = GetWeakSourceByName(nonMatchingSceneName.c_str());
@@ -793,12 +798,13 @@ void SwitcherData::CheckNoMatchSwitch(bool &match, OBSWeakSource &scene,
return;
}
if (switchIfNotMatching == NoMatch::SWITCH && nonMatchingScene) {
if (switchIfNotMatching == NoMatchBehavior::SWITCH &&
nonMatchingScene) {
match = true;
scene = nonMatchingScene;
transition = nullptr;
}
if (switchIfNotMatching == NoMatch::RANDOM_SWITCH) {
if (switchIfNotMatching == NoMatchBehavior::RANDOM_SWITCH) {
match = checkRandom(scene, transition, sleep);
}
}
@@ -949,11 +955,11 @@ void AdvSceneSwitcher::SetupGeneralTab()
{
PopulateSceneSelection(ui->noMatchSwitchScene, false);
if (switcher->switchIfNotMatching == SwitcherData::NoMatch::SWITCH) {
if (switcher->switchIfNotMatching == NoMatchBehavior::SWITCH) {
ui->noMatchSwitch->setChecked(true);
ui->noMatchSwitchScene->setEnabled(true);
} else if (switcher->switchIfNotMatching ==
SwitcherData::NoMatch::NO_SWITCH) {
NoMatchBehavior::NO_SWITCH) {
ui->noMatchDontSwitch->setChecked(true);
ui->noMatchSwitchScene->setEnabled(false);
} else {

View File

@@ -119,7 +119,7 @@ void SwitcherData::writeToStatusFile(const QString &msg)
if (file.open(QIODevice::ReadWrite)) {
QTextStream stream(&file);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
stream << msg << endl;
stream << msg << "\n";
#else
stream << msg << Qt::endl;
#endif

View File

@@ -143,8 +143,7 @@ void AdvSceneSwitcher::SetupRandomTab()
border-radius: 7px; \
border-color: rgb(0,0,0,0) \
}");
if (switcher->switchIfNotMatching !=
SwitcherData::NoMatch::RANDOM_SWITCH) {
if (switcher->switchIfNotMatching != NoMatchBehavior::RANDOM_SWITCH) {
if (!switcher->disableHints) {
PulseWidget(ui->randomDisabledWarning, QColor(Qt::red),
QColor(0, 0, 0, 0));

View File

@@ -1,5 +1,6 @@
#include "platform-funcs.hpp"
#include "hotkey.hpp"
#include "log-helper.hpp"
#include <X11/Xlib.h>
#include <X11/Xatom.h>
@@ -25,9 +26,10 @@
#include <QStringList>
#include <QRegularExpression>
#include <QLibrary>
#ifdef USE_PROCPS
#ifdef PROCPS_AVAILABLE
#include <proc/readproc.h>
#else
#endif
#ifdef PROCPS2_AVAILABLE
#include <libproc2/pids.h>
#endif
#include <fstream>
@@ -53,6 +55,31 @@ std::chrono::high_resolution_clock::time_point lastMouseLeftClickTime{};
std::chrono::high_resolution_clock::time_point lastMouseMiddleClickTime{};
std::chrono::high_resolution_clock::time_point lastMouseRightClickTime{};
static QLibrary *libprocps = nullptr;
#ifdef PROCPS_AVAILABLE
typedef PROCTAB *(*openproc_func)(int flags);
typedef void (*closeproc_func)(PROCTAB *PT);
typedef proc_t *(*readproc_func)(PROCTAB *__restrict const PT,
proc_t *__restrict p);
static openproc_func openproc_ = nullptr;
static closeproc_func closeproc_ = nullptr;
static readproc_func readproc_ = nullptr;
#endif
static bool libprocpsSupported = false;
static QLibrary *libproc2 = nullptr;
#ifdef PROCPS2_AVAILABLE
typedef int (*procps_pids_new_func)(struct pids_info **info,
enum pids_item *items, int numitems);
typedef struct pids_stack *(*procps_pids_get_func)(struct pids_info *info,
enum pids_fetch_type which);
typedef int (*procps_pids_unref_func)(struct pids_info **info);
static procps_pids_new_func procps_pids_new_ = nullptr;
static procps_pids_get_func procps_pids_get_ = nullptr;
static procps_pids_unref_func procps_pids_unref_ = nullptr;
#endif
static bool libprocps2Supported = false;
Display *disp()
{
if (!xdisplay) {
@@ -338,46 +365,57 @@ std::optional<std::string> GetTextInWindow(const std::string &)
return {};
}
#ifdef USE_PROCPS
void GetProcessList(QStringList &processes)
static void getProcessListProcps(QStringList &processes)
{
processes.clear();
PROCTAB *proc = openproc(PROC_FILLSTAT);
#ifdef PROCPS_AVAILABLE
PROCTAB *proc = openproc_(PROC_FILLSTAT);
proc_t proc_info;
memset(&proc_info, 0, sizeof(proc_info));
while (readproc(proc, &proc_info) != NULL) {
while (readproc_(proc, &proc_info) != NULL) {
QString procName(proc_info.cmd);
if (!procName.isEmpty() && !processes.contains(procName)) {
processes << procName;
}
}
closeproc(proc);
closeproc_(proc);
#endif
}
#else
void GetProcessList(QStringList &processes)
static void getProcessListProcps2(QStringList &processes)
{
processes.clear();
#ifdef PROCPS2_AVAILABLE
struct pids_info *info = NULL;
struct pids_stack *stack;
enum pids_item Items[] = {
PIDS_CMD,
};
if (procps_pids_new(&info, Items, sizeof(Items) / sizeof(Items[0])) <
if (procps_pids_new_(&info, Items, sizeof(Items) / sizeof(Items[0])) <
0) {
return;
}
while ((stack = procps_pids_get(info, PIDS_FETCH_TASKS_ONLY))) {
while ((stack = procps_pids_get_(info, PIDS_FETCH_TASKS_ONLY))) {
auto cmd = PIDS_VAL(0, str, stack, info);
QString procName(cmd);
if (!procName.isEmpty() && !processes.contains(procName)) {
processes << procName;
}
}
procps_pids_unref(&info);
}
procps_pids_unref_(&info);
#endif
}
void GetProcessList(QStringList &processes)
{
processes.clear();
if (libprocpsSupported) {
getProcessListProcps(processes);
return;
}
if (libprocps2Supported) {
getProcessListProcps2(processes);
}
}
long getForegroundProcessPid()
{
@@ -633,6 +671,58 @@ void PressKeys(const std::vector<HotkeyType> keys, int duration)
XFlush(display);
}
static void initXtst()
{
libXtstHandle = new QLibrary("libXtst", nullptr);
pressFunc = (keyPressFunc)libXtstHandle->resolve("XTestFakeKeyEvent");
int _;
canSimulateKeyPresses = pressFunc &&
XQueryExtension(disp(), "XTEST", &_, &_, &_);
}
static void initXss()
{
libXssHandle = new QLibrary("libXss", nullptr);
allocSSFunc = (XScreenSaverAllocInfoFunc)libXssHandle->resolve(
"XScreenSaverAllocInfo");
querySSFunc = (XScreenSaverQueryInfoFunc)libXssHandle->resolve(
"XScreenSaverQueryInfo");
int _;
canGetIdleTime = allocSSFunc && querySSFunc &&
XQueryExtension(disp(), ScreenSaverName, &_, &_, &_);
}
static void initProcps()
{
#ifdef PROCPS_AVAILABLE
libprocps = new QLibrary("libprocps", nullptr);
openproc_ = (openproc_func)libprocps->resolve("openproc");
closeproc_ = (closeproc_func)libprocps->resolve("closeproc");
readproc_ = (readproc_func)libprocps->resolve("readproc");
if (openproc_ && closeproc_ && readproc_) {
libprocpsSupported = true;
blog(LOG_INFO, "libprocps symbols resolved successfully!");
}
#endif
}
static void initProc2()
{
#ifdef PROCPS2_AVAILABLE
libproc2 = new QLibrary("libproc2", nullptr);
procps_pids_new_ =
(procps_pids_new_func)libproc2->resolve("procps_pids_new");
procps_pids_get_ =
(procps_pids_get_func)libproc2->resolve("procps_pids_get");
procps_pids_unref_ =
(procps_pids_unref_func)libproc2->resolve("procps_pids_unref");
if (procps_pids_new_ && procps_pids_get_ && procps_pids_unref_) {
libprocps2Supported = true;
blog(LOG_INFO, "libproc2 symbols resolved successfully!");
}
#endif
}
void PlatformInit()
{
auto display = disp();
@@ -640,31 +730,26 @@ void PlatformInit()
return;
}
libXtstHandle = new QLibrary("libXtst", nullptr);
pressFunc = (keyPressFunc)libXtstHandle->resolve("XTestFakeKeyEvent");
int _;
canSimulateKeyPresses = pressFunc &&
XQueryExtension(disp(), "XTEST", &_, &_, &_);
initXtst();
initXss();
initProcps();
initProc2();
}
libXssHandle = new QLibrary("libXss", nullptr);
allocSSFunc = (XScreenSaverAllocInfoFunc)libXssHandle->resolve(
"XScreenSaverAllocInfo");
querySSFunc = (XScreenSaverQueryInfoFunc)libXssHandle->resolve(
"XScreenSaverQueryInfo");
canGetIdleTime = allocSSFunc && querySSFunc &&
XQueryExtension(disp(), ScreenSaverName, &_, &_, &_);
static void cleanupHelper(QLibrary *lib)
{
if (lib) {
delete lib;
lib = nullptr;
}
}
void PlatformCleanup()
{
if (libXtstHandle) {
delete libXtstHandle;
libXtstHandle = nullptr;
}
if (libXssHandle) {
delete libXssHandle;
libXssHandle = nullptr;
}
cleanupHelper(libXtstHandle);
cleanupHelper(libXssHandle);
cleanupHelper(libprocps);
cleanupHelper(libproc2);
cleanupDisplay();
}

View File

@@ -39,6 +39,22 @@ const static std::map<MacroActionAudio::FadeType, std::string> fadeTypes = {
constexpr auto fadeInterval = std::chrono::milliseconds(100);
constexpr float minFade = 0.000001f;
// For backwards compatibility
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(29, 0, 0)
static float get_master_volume()
{
return 1.f;
}
static void set_master_volume(float)
{
return;
}
#else
auto get_master_volume = obs_get_master_volume;
auto set_master_volume = obs_set_master_volume;
#endif
void MacroActionAudio::SetFadeActive(bool value)
{
if (_action == Action::SOURCE_VOLUME) {
@@ -87,7 +103,7 @@ void MacroActionAudio::SetVolume(float vol)
obs_source_set_volume(s, vol);
obs_source_release(s);
} else {
obs_set_master_volume(vol);
set_master_volume(vol);
}
}
@@ -102,7 +118,7 @@ float MacroActionAudio::GetVolume()
curVol = obs_source_get_volume(s);
obs_source_release(s);
} else {
curVol = obs_get_master_volume();
curVol = get_master_volume();
}
return curVol;
}
@@ -287,15 +303,13 @@ std::string MacroActionAudio::GetShortDesc() const
static inline void populateActionSelection(QComboBox *list)
{
for (const auto &[action, name] : actionTypes) {
if (action == MacroActionAudio::Action::MONITOR) {
if (obs_audio_monitoring_available()) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(action));
}
} else {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(action));
if (action == MacroActionAudio::Action::MONITOR &&
!obs_audio_monitoring_available()) {
continue;
}
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(action));
}
}
@@ -473,6 +487,14 @@ void MacroActionAudioEdit::SetWidgetVisibility()
_wait->setVisible(hasVolumeControl(_entryData->_action) &&
_entryData->_fade);
// TODO: Remove this in a future version:
if (_entryData->_action != MacroActionAudio::Action::MASTER_VOLUME &&
obs_get_version() >= MAKE_SEMANTIC_VERSION(29, 0, 0)) {
_actions->removeItem(_actions->findText(obs_module_text(
actionTypes.at(MacroActionAudio::Action::MASTER_VOLUME)
.c_str())));
}
updateGeometry();
adjustSize();
}

View File

@@ -0,0 +1,257 @@
#include "macro-action-clipboard.hpp"
#include "curl-helper.hpp"
#include "switcher-data.hpp"
#include <QApplication>
#include <QClipboard>
#include <QImage>
#include <QMimeData>
namespace advss {
const std::string MacroActionClipboard::id = "clipboard";
bool MacroActionClipboard::_registered = MacroActionFactory::Register(
MacroActionClipboard::id,
{MacroActionClipboard::Create, MacroActionClipboardEdit::Create,
"AdvSceneSwitcher.action.clipboard"});
const static std::map<MacroActionClipboard::Action, std::string> actionTypes = {
{MacroActionClipboard::Action::COPY_TEXT,
"AdvSceneSwitcher.action.clipboard.type.copy.text"},
{MacroActionClipboard::Action::COPY_IMAGE,
"AdvSceneSwitcher.action.clipboard.type.copy.image"},
};
static size_t writeCallback(void *ptr, size_t size, size_t nmemb,
QByteArray *buffer)
{
buffer->append((char *)ptr, nmemb);
return size * nmemb;
}
static std::optional<QImage> getImageFromUrl(const char *url)
{
QByteArray response;
switcher->curl.SetOpt(CURLOPT_URL, url);
switcher->curl.SetOpt(CURLOPT_HTTPGET, 1L);
switcher->curl.SetOpt(CURLOPT_TIMEOUT_MS, 30000);
switcher->curl.SetOpt(CURLOPT_WRITEFUNCTION, writeCallback);
switcher->curl.SetOpt(CURLOPT_WRITEDATA, &response);
auto code = switcher->curl.Perform();
if (code != CURLE_OK) {
blog(LOG_WARNING,
"Retrieving image failed in %s with error: %s", __func__,
switcher->curl.GetError(code));
return {};
}
return QImage::fromData(response);
}
static void setMimeTypeParams(ClipboardQueueParams *params,
QClipboard *clipboard)
{
auto mimeData = clipboard->mimeData();
if (!mimeData) {
return;
}
auto mimeTypeList = mimeData->formats();
if (mimeTypeList.empty()) {
return;
}
params->mimeTypePrimary = mimeTypeList.first().toStdString();
params->mimeTypeAll = mimeTypeList.join(" ").toStdString();
}
static void copyText(void *param)
{
auto params = static_cast<ClipboardTextQueueParams *>(param);
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(params->text.c_str());
setMimeTypeParams(params, clipboard);
}
static void copyImageFromUrl(void *param)
{
auto params = static_cast<ClipboardImageQueueParams *>(param);
auto url = params->url.c_str();
auto image = getImageFromUrl(url);
if (!image || (*image).isNull()) {
blog(LOG_WARNING, "Failed to convert %s URL to image!", url);
return;
}
QClipboard *clipboard = QApplication::clipboard();
clipboard->setImage(*image);
setMimeTypeParams(params, clipboard);
}
bool MacroActionClipboard::PerformAction()
{
switch (_action) {
case Action::COPY_TEXT: {
ClipboardTextQueueParams params{"", "", _text};
obs_queue_task(OBS_TASK_UI, copyText, &params, true);
SetTempVarValues(params);
break;
}
case Action::COPY_IMAGE: {
ClipboardImageQueueParams params{"", "", _url};
obs_queue_task(OBS_TASK_UI, copyImageFromUrl, &params, true);
SetTempVarValues(params);
break;
}
default:
break;
}
return true;
}
void MacroActionClipboard::SetupTempVars()
{
MacroAction::SetupTempVars();
AddTempvar(
"mimeType.primary",
obs_module_text(
"AdvSceneSwitcher.tempVar.clipboard.mimeType.primary"),
obs_module_text(
"AdvSceneSwitcher.tempVar.clipboard.mimeType.primary.description"));
AddTempvar(
"mimeType.all",
obs_module_text(
"AdvSceneSwitcher.tempVar.clipboard.mimeType.all"),
obs_module_text(
"AdvSceneSwitcher.tempVar.clipboard.mimeType.all.description"));
}
void MacroActionClipboard::SetTempVarValues(const ClipboardQueueParams &params)
{
SetTempVarValue("mimeType.primary", params.mimeTypePrimary);
SetTempVarValue("mimeType.all", params.mimeTypeAll);
}
bool MacroActionClipboard::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
obs_data_set_int(obj, "action", static_cast<int>(_action));
_text.Save(obj, "text");
_url.Save(obj, "url");
return true;
}
bool MacroActionClipboard::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
_text.Load(obj, "text");
_url.Load(obj, "url");
return true;
}
static inline void populateActionSelection(QComboBox *list)
{
for (const auto &[action, name] : actionTypes) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(action));
}
}
MacroActionClipboardEdit::MacroActionClipboardEdit(
QWidget *parent, std::shared_ptr<MacroActionClipboard> entryData)
: QWidget(parent),
_actions(new FilterComboBox(this)),
_text(new VariableLineEdit(this)),
_url(new VariableLineEdit(this))
{
populateActionSelection(_actions);
_url->setToolTip(obs_module_text(
"AdvSceneSwitcher.action.clipboard.copy.image.url.tooltip"));
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionChanged(int)));
QWidget::connect(_text, SIGNAL(editingFinished()), this,
SLOT(TextChanged()));
QWidget::connect(_url, SIGNAL(editingFinished()), this,
SLOT(UrlChanged()));
auto mainLayout = new QHBoxLayout();
mainLayout->addWidget(_actions);
mainLayout->addWidget(_text);
mainLayout->addWidget(_url);
setLayout(mainLayout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionClipboardEdit::ActionChanged(int index)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_action = static_cast<MacroActionClipboard::Action>(
_actions->itemData(index).toInt());
SetWidgetVisibility();
}
void MacroActionClipboardEdit::TextChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_text = _text->text().toStdString();
}
void MacroActionClipboardEdit::UrlChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_url = _url->text().toStdString();
}
void MacroActionClipboardEdit::SetWidgetVisibility()
{
_text->setVisible(_entryData->_action ==
MacroActionClipboard::Action::COPY_TEXT);
_url->setVisible(_entryData->_action ==
MacroActionClipboard::Action::COPY_IMAGE);
adjustSize();
updateGeometry();
}
void MacroActionClipboardEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_actions->setCurrentIndex(
_actions->findData(static_cast<int>(_entryData->_action)));
_text->setText(_entryData->_text);
_url->setText(_entryData->_url);
SetWidgetVisibility();
}
} // namespace advss

View File

@@ -0,0 +1,89 @@
#pragma once
#include "macro-action-edit.hpp"
#include "filter-combo-box.hpp"
#include <variable-line-edit.hpp>
namespace advss {
struct ClipboardQueueParams {
std::string mimeTypePrimary;
std::string mimeTypeAll;
};
struct ClipboardTextQueueParams : ClipboardQueueParams {
StringVariable text;
};
struct ClipboardImageQueueParams : ClipboardQueueParams {
StringVariable url;
};
class MacroActionClipboard : public MacroAction {
public:
MacroActionClipboard(Macro *m) : MacroAction(m) {}
static std::shared_ptr<MacroAction> Create(Macro *m)
{
return std::make_shared<MacroActionClipboard>(m);
}
std::string GetId() const { return id; };
bool PerformAction();
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
enum class Action {
COPY_TEXT,
COPY_IMAGE,
};
Action _action = Action::COPY_TEXT;
StringVariable _text = obs_module_text(
"AdvSceneSwitcher.action.clipboard.copy.text.text.placeholder");
StringVariable _url = obs_module_text(
"AdvSceneSwitcher.action.clipboard.copy.image.url.placeholder");
private:
void SetupTempVars();
void SetTempVarValues(const ClipboardQueueParams &params);
static bool _registered;
static const std::string id;
};
class MacroActionClipboardEdit : public QWidget {
Q_OBJECT
public:
MacroActionClipboardEdit(
QWidget *parent,
std::shared_ptr<MacroActionClipboard> entryData = nullptr);
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionClipboardEdit(
parent, std::dynamic_pointer_cast<MacroActionClipboard>(
action));
}
void UpdateEntryData();
protected:
std::shared_ptr<MacroActionClipboard> _entryData;
private slots:
void ActionChanged(int index);
void TextChanged();
void UrlChanged();
private:
void SetWidgetVisibility();
bool _loading = true;
FilterComboBox *_actions;
VariableLineEdit *_text;
VariableLineEdit *_url;
};
} // namespace advss

View File

@@ -138,6 +138,7 @@ void MacroActionEdit::ActionSelectionChanged(const QString &text)
_entryData->reset();
*_entryData = MacroActionFactory::Create(id, macro);
(*_entryData)->SetIndex(idx);
(*_entryData)->PostLoad();
}
auto widget = MacroActionFactory::CreateWidget(id, this, *_entryData);
QWidget::connect(widget, SIGNAL(HeaderInfoChanged(const QString &)),
@@ -204,7 +205,7 @@ void MacroActionEdit::SetEnableAppearance(bool value)
SetDisableEffect(!value);
}
std::shared_ptr<MacroSegment> MacroActionEdit::Data()
std::shared_ptr<MacroSegment> MacroActionEdit::Data() const
{
return *_entryData;
}
@@ -236,6 +237,7 @@ void AdvSceneSwitcher::AddMacroAction(int idx)
auto data = obs_data_create();
macro->Actions().at(idx - 1)->Save(data);
macro->Actions().at(idx)->Load(data);
macro->Actions().at(idx)->PostLoad();
obs_data_release(data);
}
macro->UpdateActionIndices();
@@ -525,6 +527,7 @@ void AdvSceneSwitcher::AddMacroElseAction(int idx)
macro->ElseActions().at(idx - 1)->Save(data);
macro->ElseActions().at(idx)->Load(data);
}
macro->ElseActions().at(idx)->PostLoad();
macro->UpdateElseActionIndices();
ui->elseActionsList->Insert(
idx, new MacroActionEdit(

View File

@@ -50,7 +50,7 @@ private slots:
void UpdateActionState();
private:
std::shared_ptr<MacroSegment> Data();
std::shared_ptr<MacroSegment> Data() const;
void SetDisableEffect(bool);
void SetEnableAppearance(bool);

View File

@@ -21,26 +21,69 @@ const static std::map<MacroActionFilter::Action, std::string> actionTypes = {
"AdvSceneSwitcher.action.filter.type.settings"},
};
bool MacroActionFilter::PerformAction()
const static std::map<MacroActionFilter::SettingsInputMethod, std::string>
inputMethods = {
{MacroActionFilter::SettingsInputMethod::INDIVIDUAL_MANUAL,
"AdvSceneSwitcher.action.filter.inputMethod.individualManual"},
{MacroActionFilter::SettingsInputMethod::INDIVIDUAL_TEMPVAR,
"AdvSceneSwitcher.action.filter.inputMethod.individualTempvar"},
{MacroActionFilter::SettingsInputMethod::JSON_STRING,
"AdvSceneSwitcher.action.filter.inputMethod.json"},
};
static void performActionHelper(
MacroActionFilter::Action action, const OBSWeakSource &filter,
MacroActionFilter::SettingsInputMethod settingsInputMethod,
const SourceSetting &setting, const std::string &manualSettingValue,
const TempVariableRef &tempVar, Macro *macro,
const StringVariable &settingsString)
{
auto s = obs_weak_source_get_source(_filter.GetFilter(_source));
switch (_action) {
case Action::ENABLE:
obs_source_set_enabled(s, true);
OBSSourceAutoRelease source = obs_weak_source_get_source(filter);
switch (action) {
case MacroActionFilter::Action::ENABLE:
obs_source_set_enabled(source, true);
break;
case Action::DISABLE:
obs_source_set_enabled(s, false);
case MacroActionFilter::Action::DISABLE:
obs_source_set_enabled(source, false);
break;
case Action::TOGGLE:
obs_source_set_enabled(s, !obs_source_enabled(s));
case MacroActionFilter::Action::TOGGLE:
obs_source_set_enabled(source, !obs_source_enabled(source));
break;
case Action::SETTINGS:
SetSourceSettings(s, _settings);
case MacroActionFilter::Action::SETTINGS:
switch (settingsInputMethod) {
case MacroActionFilter::SettingsInputMethod::INDIVIDUAL_MANUAL:
SetSourceSetting(source, setting, manualSettingValue);
break;
case MacroActionFilter::SettingsInputMethod::INDIVIDUAL_TEMPVAR: {
auto var = tempVar.GetTempVariable(macro);
if (!var) {
break;
}
auto value = var->Value();
if (!value) {
break;
}
SetSourceSetting(source, setting, *value);
break;
}
case MacroActionFilter::SettingsInputMethod::JSON_STRING:
SetSourceSettings(source, settingsString);
break;
}
break;
default:
break;
}
obs_source_release(s);
}
bool MacroActionFilter::PerformAction()
{
auto filters = _filter.GetFilters(_source);
for (const auto &filter : filters) {
performActionHelper(_action, filter, _settingsInputMethod,
_setting, _manualSettingValue, _tempVar,
GetMacro(), _settingsString);
}
return true;
}
@@ -64,7 +107,12 @@ bool MacroActionFilter::Save(obs_data_t *obj) const
_source.Save(obj);
_filter.Save(obj, "filter");
obs_data_set_int(obj, "action", static_cast<int>(_action));
_settings.Save(obj, "settings");
obs_data_set_int(obj, "inputMethod",
static_cast<int>(_settingsInputMethod));
_setting.Save(obj);
_manualSettingValue.Save(obj, "manualSettingValue");
_tempVar.Save(obj);
_settingsString.Save(obj, "settings");
obs_data_set_int(obj, "version", 1);
return true;
}
@@ -85,7 +133,17 @@ bool MacroActionFilter::Load(obs_data_t *obj)
} else {
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
}
_settings.Load(obj, "settings");
// TODO: Remove fallback in future version
if (obs_data_has_user_value(obj, "inputMethod")) {
_settingsInputMethod = static_cast<SettingsInputMethod>(
obs_data_get_int(obj, "inputMethod"));
} else {
_settingsInputMethod = SettingsInputMethod::JSON_STRING;
}
_setting.Load(obj);
_settingsString.Load(obj, "settings");
_manualSettingValue.Load(obj, "manualSettingValue");
_tempVar.Load(obj, GetMacro());
return true;
}
@@ -104,6 +162,15 @@ static inline void populateActionSelection(QComboBox *list)
}
}
static inline void populateSettingsInputMethods(QComboBox *list)
{
list->clear();
for (const auto &[value, name] : inputMethods) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(value));
}
}
MacroActionFilterEdit::MacroActionFilterEdit(
QWidget *parent, std::shared_ptr<MacroActionFilter> entryData)
: QWidget(parent),
@@ -112,7 +179,12 @@ MacroActionFilterEdit::MacroActionFilterEdit(
_actions(new QComboBox()),
_getSettings(new QPushButton(obs_module_text(
"AdvSceneSwitcher.action.filter.getSettings"))),
_settings(new VariableTextEdit(this))
_settingsLayout(new QHBoxLayout()),
_settingsInputMethods(new QComboBox(this)),
_manualSettingValue(new VariableTextEdit(this, 5, 1, 1)),
_tempVars(new TempVariableSelection(this)),
_filterSettings(new SourceSettingSelection(this)),
_settingsString(new VariableTextEdit(this))
{
_filters->setSizeAdjustPolicy(QComboBox::AdjustToContents);
@@ -121,6 +193,8 @@ MacroActionFilterEdit::MacroActionFilterEdit(
sources.sort();
_sources->SetSourceNameList(sources);
populateSettingsInputMethods(_settingsInputMethods);
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionChanged(int)));
QWidget::connect(_sources,
@@ -131,27 +205,48 @@ MacroActionFilterEdit::MacroActionFilterEdit(
SLOT(FilterChanged(const FilterSelection &)));
QWidget::connect(_getSettings, SIGNAL(clicked()), this,
SLOT(GetSettingsClicked()));
QWidget::connect(_settings, SIGNAL(textChanged()), this,
SLOT(SettingsChanged()));
QWidget::connect(_settingsString, SIGNAL(textChanged()), this,
SLOT(SettingsStringChanged()));
QWidget::connect(_tempVars,
SIGNAL(SelectionChanged(const TempVariableRef &)),
this, SLOT(SelectionChanged(const TempVariableRef &)));
QWidget::connect(_settingsInputMethods,
SIGNAL(currentIndexChanged(int)), this,
SLOT(SettingsInputMethodChanged(int)));
QWidget::connect(_manualSettingValue, SIGNAL(textChanged()), this,
SLOT(ManualSettingsValueChanged()));
QWidget::connect(_filterSettings,
SIGNAL(SelectionChanged(const SourceSetting &)), this,
SLOT(SelectionChanged(const SourceSetting &)));
QHBoxLayout *entryLayout = new QHBoxLayout;
auto entrylayout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{sources}}", _sources}, {"{{filters}}", _filters},
{"{{actions}}", _actions}, {"{{settings}}", _settings},
{"{{sources}}", _sources},
{"{{filters}}", _filters},
{"{{actions}}", _actions},
{"{{settings}}", _filterSettings},
{"{{settingsInputMethod}}", _settingsInputMethods},
{"{{settingValue}}", _manualSettingValue},
{"{{tempVar}}", _tempVars},
{"{{getSettings}}", _getSettings},
};
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.filter.entry"),
entryLayout, widgetPlaceholders);
entrylayout, widgetPlaceholders);
_settingsLayout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.filter.entry.settings"),
_settingsLayout, widgetPlaceholders);
QHBoxLayout *buttonLayout = new QHBoxLayout;
auto buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(_getSettings);
buttonLayout->addStretch();
buttonLayout->setContentsMargins(0, 0, 0, 0);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(entryLayout);
mainLayout->addWidget(_settings);
auto mainLayout = new QVBoxLayout;
mainLayout->addLayout(entrylayout);
mainLayout->addLayout(_settingsLayout);
mainLayout->addWidget(_settingsString);
mainLayout->addLayout(buttonLayout);
setLayout(mainLayout);
@@ -169,12 +264,16 @@ void MacroActionFilterEdit::UpdateEntryData()
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_sources->SetSource(_entryData->_source);
_filters->SetFilter(_entryData->_source, _entryData->_filter);
_settings->setPlainText(_entryData->_settings);
SetWidgetVisibility(_entryData->_action ==
MacroActionFilter::Action::SETTINGS);
adjustSize();
updateGeometry();
_settingsString->setPlainText(_entryData->_settingsString);
const auto filters =
_entryData->_filter.GetFilters(_entryData->_source);
_filterSettings->SetSource(filters.empty() ? nullptr : filters.at(0));
_filterSettings->SetSetting(_entryData->_setting);
_settingsInputMethods->setCurrentIndex(_settingsInputMethods->findData(
static_cast<int>(_entryData->_settingsInputMethod)));
_tempVars->SetVariable(_entryData->_tempVar);
_manualSettingValue->setPlainText(_entryData->_manualSettingValue);
SetWidgetVisibility();
}
void MacroActionFilterEdit::SourceChanged(const SourceSelection &source)
@@ -193,8 +292,13 @@ void MacroActionFilterEdit::FilterChanged(const FilterSelection &filter)
return;
}
auto lock = LockContext();
_entryData->_filter = filter;
{
auto lock = LockContext();
_entryData->_filter = filter;
}
const auto filters =
_entryData->_filter.GetFilters(_entryData->_source);
_filterSettings->SetSource(filters.empty() ? nullptr : filters.at(0));
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
@@ -207,38 +311,130 @@ void MacroActionFilterEdit::ActionChanged(int value)
auto lock = LockContext();
_entryData->_action = static_cast<MacroActionFilter::Action>(value);
SetWidgetVisibility(_entryData->_action ==
MacroActionFilter::Action::SETTINGS);
SetWidgetVisibility();
}
void MacroActionFilterEdit::GetSettingsClicked()
{
if (_loading || !_entryData ||
!_entryData->_filter.GetFilter(_entryData->_source)) {
_entryData->_filter.GetFilters(_entryData->_source).empty()) {
return;
}
_settings->setPlainText(FormatJsonString(GetSourceSettings(
_entryData->_filter.GetFilter(_entryData->_source))));
switch (_entryData->_settingsInputMethod) {
case MacroActionFilter::SettingsInputMethod::INDIVIDUAL_MANUAL: {
const auto filters =
_entryData->_filter.GetFilters(_entryData->_source);
_manualSettingValue->setPlainText(GetSourceSettingValue(
filters.empty() ? nullptr : filters.at(0),
_entryData->_setting));
break;
}
case MacroActionFilter::SettingsInputMethod::INDIVIDUAL_TEMPVAR:
break;
case MacroActionFilter::SettingsInputMethod::JSON_STRING: {
const auto filters =
_entryData->_filter.GetFilters(_entryData->_source);
_settingsString->setPlainText(
FormatJsonString(GetSourceSettings(
filters.empty() ? nullptr : filters.at(0))));
break;
}
}
}
void MacroActionFilterEdit::SettingsChanged()
void MacroActionFilterEdit::SettingsStringChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_settings = _settings->toPlainText().toStdString();
_entryData->_settingsString =
_settingsString->toPlainText().toStdString();
adjustSize();
updateGeometry();
}
void MacroActionFilterEdit::SetWidgetVisibility(bool showSettings)
void MacroActionFilterEdit::SettingsInputMethodChanged(int idx)
{
_settings->setVisible(showSettings);
_getSettings->setVisible(showSettings);
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_settingsInputMethod =
static_cast<MacroActionFilter::SettingsInputMethod>(
_settingsInputMethods->itemData(idx).toInt());
SetWidgetVisibility();
}
void MacroActionFilterEdit::SelectionChanged(const TempVariableRef &var)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_tempVar = var;
}
void MacroActionFilterEdit::SelectionChanged(const SourceSetting &setting)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_setting = setting;
}
void MacroActionFilterEdit::ManualSettingsValueChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_manualSettingValue =
_manualSettingValue->toPlainText().toStdString();
adjustSize();
updateGeometry();
}
void MacroActionFilterEdit::SetWidgetVisibility()
{
SetLayoutVisible(_settingsLayout,
_entryData->_action ==
MacroActionFilter::Action::SETTINGS);
_filterSettings->setVisible(
_entryData->_action == MacroActionFilter::Action::SETTINGS &&
_entryData->_settingsInputMethod !=
MacroActionFilter::SettingsInputMethod::JSON_STRING);
_settingsString->setVisible(
_entryData->_action == MacroActionFilter::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionFilter::SettingsInputMethod::JSON_STRING);
_getSettings->setVisible(_entryData->_action ==
MacroActionFilter::Action::SETTINGS);
_tempVars->setVisible(_entryData->_action ==
MacroActionFilter::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionFilter::SettingsInputMethod::
INDIVIDUAL_TEMPVAR);
if (_entryData->_action == MacroActionFilter::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionFilter::SettingsInputMethod::INDIVIDUAL_MANUAL) {
RemoveStretchIfPresent(_settingsLayout);
_manualSettingValue->show();
} else {
AddStretchIfNecessary(_settingsLayout);
_manualSettingValue->hide();
}
adjustSize();
updateGeometry();
}

View File

@@ -3,6 +3,7 @@
#include "variable-text-edit.hpp"
#include "source-selection.hpp"
#include "filter-selection.hpp"
#include "source-setting.hpp"
#include <QComboBox>
#include <QSpinBox>
@@ -31,10 +32,21 @@ public:
SETTINGS,
};
enum class SettingsInputMethod {
INDIVIDUAL_MANUAL,
INDIVIDUAL_TEMPVAR,
JSON_STRING,
};
SettingsInputMethod _settingsInputMethod =
SettingsInputMethod::INDIVIDUAL_MANUAL;
SourceSelection _source;
FilterSelection _filter;
Action _action = Action::ENABLE;
StringVariable _settings = "";
StringVariable _settingsString = "";
TempVariableRef _tempVar;
StringVariable _manualSettingValue = "";
SourceSetting _setting;
private:
static bool _registered;
@@ -62,7 +74,11 @@ private slots:
void FilterChanged(const FilterSelection &);
void ActionChanged(int value);
void GetSettingsClicked();
void SettingsChanged();
void SettingsStringChanged();
void SettingsInputMethodChanged(int);
void SelectionChanged(const TempVariableRef &);
void SelectionChanged(const SourceSetting &);
void ManualSettingsValueChanged();
signals:
void HeaderInfoChanged(const QString &);
@@ -71,11 +87,17 @@ protected:
FilterSelectionWidget *_filters;
QComboBox *_actions;
QPushButton *_getSettings;
VariableTextEdit *_settings;
QHBoxLayout *_settingsLayout;
QComboBox *_settingsInputMethods;
VariableTextEdit *_manualSettingValue;
TempVariableSelection *_tempVars;
SourceSettingSelection *_filterSettings;
VariableTextEdit *_settingsString;
std::shared_ptr<MacroActionFilter> _entryData;
private:
void SetWidgetVisibility(bool);
void SetWidgetVisibility();
bool _loading = true;
};

View File

@@ -10,22 +10,45 @@ bool MacroActionMedia::_registered = MacroActionFactory::Register(
{MacroActionMedia::Create, MacroActionMediaEdit::Create,
"AdvSceneSwitcher.action.media"});
const static std::map<MediaAction, std::string> actionTypes = {
{MediaAction::PLAY, "AdvSceneSwitcher.action.media.type.play"},
{MediaAction::PAUSE, "AdvSceneSwitcher.action.media.type.pause"},
{MediaAction::STOP, "AdvSceneSwitcher.action.media.type.stop"},
{MediaAction::RESTART, "AdvSceneSwitcher.action.media.type.restart"},
{MediaAction::NEXT, "AdvSceneSwitcher.action.media.type.next"},
{MediaAction::PREVIOUS, "AdvSceneSwitcher.action.media.type.previous"},
{MediaAction::SEEK, "AdvSceneSwitcher.action.media.type.seek"},
const static std::map<MacroActionMedia::Action, std::string> actionTypes = {
{MacroActionMedia::Action::PLAY,
"AdvSceneSwitcher.action.media.type.play"},
{MacroActionMedia::Action::PAUSE,
"AdvSceneSwitcher.action.media.type.pause"},
{MacroActionMedia::Action::STOP,
"AdvSceneSwitcher.action.media.type.stop"},
{MacroActionMedia::Action::RESTART,
"AdvSceneSwitcher.action.media.type.restart"},
{MacroActionMedia::Action::NEXT,
"AdvSceneSwitcher.action.media.type.next"},
{MacroActionMedia::Action::PREVIOUS,
"AdvSceneSwitcher.action.media.type.previous"},
{MacroActionMedia::Action::SEEK_DURATION,
"AdvSceneSwitcher.action.media.type.seek.duration"},
{MacroActionMedia::Action::SEEK_PERCENTAGE,
"AdvSceneSwitcher.action.media.type.seek.percentage"},
};
std::string MacroActionMedia::GetShortDesc() const
{
return _mediaSource.ToString();
}
void MacroActionMedia::SeekToPercentage(obs_source_t *source) const
{
auto totalTimeMs = obs_source_media_get_duration(source);
auto percentageTimeMs = round(totalTimeMs * _seekPercentage / 100);
obs_source_media_set_time(source, percentageTimeMs);
}
bool MacroActionMedia::PerformAction()
{
auto source = obs_weak_source_get_source(_mediaSource.GetSource());
obs_media_state state = obs_source_media_get_state(source);
switch (_action) {
case MediaAction::PLAY:
case Action::PLAY:
if (state == OBS_MEDIA_STATE_STOPPED ||
state == OBS_MEDIA_STATE_ENDED) {
obs_source_media_restart(source);
@@ -33,27 +56,31 @@ bool MacroActionMedia::PerformAction()
obs_source_media_play_pause(source, false);
}
break;
case MediaAction::PAUSE:
case Action::PAUSE:
obs_source_media_play_pause(source, true);
break;
case MediaAction::STOP:
case Action::STOP:
obs_source_media_stop(source);
break;
case MediaAction::RESTART:
case Action::RESTART:
obs_source_media_restart(source);
break;
case MediaAction::NEXT:
case Action::NEXT:
obs_source_media_next(source);
break;
case MediaAction::PREVIOUS:
case Action::PREVIOUS:
obs_source_media_previous(source);
break;
case MediaAction::SEEK:
obs_source_media_set_time(source, _seek.Milliseconds());
case Action::SEEK_DURATION:
obs_source_media_set_time(source, _seekDuration.Milliseconds());
break;
case Action::SEEK_PERCENTAGE:
SeekToPercentage(source);
break;
default:
break;
}
obs_source_release(source);
return true;
}
@@ -73,39 +100,42 @@ void MacroActionMedia::LogAction() const
bool MacroActionMedia::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
_mediaSource.Save(obj, "mediaSource");
obs_data_set_int(obj, "action", static_cast<int>(_action));
_seek.Save(obj);
_seekDuration.Save(obj);
_seekPercentage.Save(obj, "seekPercentage");
_mediaSource.Save(obj, "mediaSource");
return true;
}
bool MacroActionMedia::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
_seekDuration.Load(obj);
_seekPercentage.Load(obj, "seekPercentage");
_mediaSource.Load(obj, "mediaSource");
_action = static_cast<MediaAction>(obs_data_get_int(obj, "action"));
_seek.Load(obj);
return true;
}
std::string MacroActionMedia::GetShortDesc() const
{
return _mediaSource.ToString();
return true;
}
static inline void populateActionSelection(QComboBox *list)
{
for (auto entry : actionTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
for (const auto &[_, name] : actionTypes) {
list->addItem(obs_module_text(name.c_str()));
}
}
MacroActionMediaEdit::MacroActionMediaEdit(
QWidget *parent, std::shared_ptr<MacroActionMedia> entryData)
: QWidget(parent),
_sources(new SourceSelectionWidget(this, QStringList(), true)),
_actions(new QComboBox()),
_seek(new DurationSelection())
_seekDuration(new DurationSelection()),
_seekPercentage(new SliderSpinBox(
0, 100,
obs_module_text(
"AdvSceneSwitcher.action.media.seek.percentage.label"))),
_sources(new SourceSelectionWidget(this, QStringList(), true))
{
populateActionSelection(_actions);
auto sources = GetMediaSourceNames();
@@ -114,39 +144,67 @@ MacroActionMediaEdit::MacroActionMediaEdit(
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionChanged(int)));
QWidget::connect(_seekDuration,
SIGNAL(DurationChanged(const Duration &)), this,
SLOT(SeekDurationChanged(const Duration &)));
QWidget::connect(
_seekPercentage,
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
this,
SLOT(SeekPercentageChanged(const NumberVariable<double> &)));
QWidget::connect(_sources,
SIGNAL(SourceChanged(const SourceSelection &)), this,
SLOT(SourceChanged(const SourceSelection &)));
QWidget::connect(_seek, SIGNAL(DurationChanged(const Duration &)), this,
SLOT(DurationChanged(const Duration &)));
QHBoxLayout *mainLayout = new QHBoxLayout;
auto layout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{mediaSources}}", _sources},
{"{{actions}}", _actions},
{"{{duration}}", _seek},
{"{{seekDuration}}", _seekDuration},
{"{{seekPercentage}}", _seekPercentage},
{"{{mediaSources}}", _sources},
};
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.media.entry"),
mainLayout, widgetPlaceholders);
setLayout(mainLayout);
layout, widgetPlaceholders);
setLayout(layout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionMediaEdit::UpdateEntryData()
void MacroActionMediaEdit::ActionChanged(int value)
{
if (!_entryData) {
if (_loading || !_entryData) {
return;
}
_sources->SetSource(_entryData->_mediaSource);
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_seek->SetDuration(_entryData->_seek);
auto lock = LockContext();
_entryData->_action = static_cast<MacroActionMedia::Action>(value);
SetWidgetVisibility();
}
void MacroActionMediaEdit::SeekDurationChanged(const Duration &seekDuration)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_seekDuration = seekDuration;
}
void MacroActionMediaEdit::SeekPercentageChanged(
const NumberVariable<double> &seekPercentage)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_seekPercentage = seekPercentage;
}
void MacroActionMediaEdit::SourceChanged(const SourceSelection &source)
{
if (_loading || !_entryData) {
@@ -159,34 +217,32 @@ void MacroActionMediaEdit::SourceChanged(const SourceSelection &source)
QString::fromStdString(_entryData->GetShortDesc()));
}
void MacroActionMediaEdit::ActionChanged(int value)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_action = static_cast<MediaAction>(value);
SetWidgetVisibility();
}
void MacroActionMediaEdit::DurationChanged(const Duration &dur)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_seek = dur;
}
void MacroActionMediaEdit::SetWidgetVisibility()
{
if (!_entryData) {
return;
}
_seek->setVisible(_entryData->_action == MediaAction::SEEK);
_seekDuration->setVisible(_entryData->_action ==
MacroActionMedia::Action::SEEK_DURATION);
_seekPercentage->setVisible(_entryData->_action ==
MacroActionMedia::Action::SEEK_PERCENTAGE);
adjustSize();
}
void MacroActionMediaEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_seekDuration->SetDuration(_entryData->_seekDuration);
_seekPercentage->SetDoubleValue(_entryData->_seekPercentage);
_sources->SetSource(_entryData->_mediaSource);
SetWidgetVisibility();
}
} // namespace advss

View File

@@ -1,42 +1,45 @@
#pragma once
#include "macro-action-edit.hpp"
#include "duration-control.hpp"
#include "slider-spinbox.hpp"
#include "source-selection.hpp"
#include <QSpinBox>
#include <QHBoxLayout>
namespace advss {
enum class MediaAction {
PLAY,
PAUSE,
STOP,
RESTART,
NEXT,
PREVIOUS,
SEEK,
};
class MacroActionMedia : public MacroAction {
public:
MacroActionMedia(Macro *m) : MacroAction(m) {}
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)
{
return std::make_shared<MacroActionMedia>(m);
}
std::string GetId() const { return id; };
std::string GetShortDesc() const;
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
enum class Action {
PLAY,
PAUSE,
STOP,
RESTART,
NEXT,
PREVIOUS,
SEEK_DURATION,
SEEK_PERCENTAGE,
};
Action _action = Action::PLAY;
Duration _seekDuration;
DoubleVariable _seekPercentage = 50;
SourceSelection _mediaSource;
MediaAction _action = MediaAction::PLAY;
Duration _seek;
private:
void SeekToPercentage(obs_source_t *source) const;
static bool _registered;
static const std::string id;
};
@@ -48,7 +51,6 @@ public:
MacroActionMediaEdit(
QWidget *parent,
std::shared_ptr<MacroActionMedia> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
@@ -56,24 +58,27 @@ public:
parent,
std::dynamic_pointer_cast<MacroActionMedia>(action));
}
void UpdateEntryData();
private slots:
void SourceChanged(const SourceSelection &);
void ActionChanged(int value);
void DurationChanged(const Duration &value);
void SeekDurationChanged(const Duration &seekDuration);
void
SeekPercentageChanged(const NumberVariable<double> &seekPercentage);
void SourceChanged(const SourceSelection &source);
signals:
void HeaderInfoChanged(const QString &);
protected:
SourceSelectionWidget *_sources;
QComboBox *_actions;
DurationSelection *_seek;
std::shared_ptr<MacroActionMedia> _entryData;
private:
void SetWidgetVisibility();
QHBoxLayout *_mainLayout;
QComboBox *_actions;
DurationSelection *_seekDuration;
SliderSpinBox *_seekPercentage;
SourceSelectionWidget *_sources;
std::shared_ptr<MacroActionMedia> _entryData;
bool _loading = true;
};

View File

@@ -1,8 +1,9 @@
#include "macro-action-plugin-state.hpp"
#include "switcher-data.hpp"
#include "plugin-state-helper.hpp"
#include "utility.hpp"
#include <thread>
#include <condition_variable>
#include <QMainWindow>
using namespace std::chrono_literals;
@@ -27,40 +28,38 @@ const static std::map<PluginStateAction, std::string> actionTypes = {
"AdvSceneSwitcher.action.pluginState.type.terminate"},
};
const static std::map<SwitcherData::NoMatch, std::string> noMatchValues = {
{SwitcherData::NoMatch::NO_SWITCH,
const static std::map<NoMatchBehavior, std::string> noMatchValues = {
{NoMatchBehavior::NO_SWITCH,
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMet.dontSwitch"},
{SwitcherData::NoMatch::SWITCH,
{NoMatchBehavior::SWITCH,
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMet.switchTo"},
{SwitcherData::NoMatch::RANDOM_SWITCH,
{NoMatchBehavior::RANDOM_SWITCH,
"AdvSceneSwitcher.generalTab.generalBehavior.onNoMet.switchToRandom"},
};
static void stopPlugin()
{
std::thread t([]() { switcher->Stop(); });
std::thread t([]() { StopPlugin(); });
t.detach();
}
static void importSettings(const std::string &path)
{
if (switcher->settingsWindowOpened) {
if (SettingsWindowIsOpened()) {
return;
}
obs_data_t *obj = obs_data_create_from_json_file(path.c_str());
OBSDataAutoRelease obj = obs_data_create_from_json_file(path.c_str());
if (!obj) {
return;
}
switcher->LoadSettings(obj);
obs_data_release(obj);
LoadPluginSettings(obj);
}
static void setNoMatchBehaviour(int value, OBSWeakSource &scene)
{
switcher->switchIfNotMatching =
static_cast<SwitcherData::NoMatch>(value);
if (switcher->switchIfNotMatching == SwitcherData::NoMatch::SWITCH) {
switcher->nonMatchingScene = scene;
SetPluginNoMatchBehavior(static_cast<NoMatchBehavior>(value));
if (GetPluginNoMatchBehavior() == NoMatchBehavior::SWITCH) {
SetNoMatchScene(scene);
}
}
@@ -342,8 +341,8 @@ void MacroActionPluginStateEdit::SetWidgetVisibility()
break;
case PluginStateAction::NO_MATCH_BEHAVIOUR:
_values->show();
if (static_cast<SwitcherData::NoMatch>(_entryData->_value) ==
SwitcherData::NoMatch::SWITCH) {
if (static_cast<NoMatchBehavior>(_entryData->_value) ==
NoMatchBehavior::SWITCH) {
_scenes->show();
}
break;

View File

@@ -1,5 +1,5 @@
#include "macro-action-scene-collection.hpp"
#include "switcher-data.hpp"
#include "plugin-state-helper.hpp"
#include "utility.hpp"
namespace advss {
@@ -17,7 +17,7 @@ bool MacroActionSceneCollection::PerformAction()
// Changing the scene collection will also reload the settings of the
// scene switcher, so to avoid issues of not being able to change
// settings ignore this action if the settings dialog is opened.
if (switcher->settingsWindowOpened) {
if (SettingsWindowIsOpened()) {
return false;
}
obs_frontend_set_current_scene_collection(_sceneCollection.c_str());

View File

@@ -145,7 +145,8 @@ bool MacroActionSwitchScene::PerformAction()
auto transition = _transition.GetTransition();
SwitchScene({scene, transition, (int)(_duration.Milliseconds())},
obs_frontend_preview_program_mode_active());
if (_blockUntilTransitionDone && scene) {
if (_blockUntilTransitionDone && scene &&
scene != switcher->currentScene) {
return WaitForTransition(scene, transition);
}
return true;

View File

@@ -32,7 +32,7 @@ void MacroActionScreenshot::CustomScreenshot(OBSWeakSource &source)
}
auto s = obs_weak_source_get_source(source);
_screenshot.~ScreenshotHelper();
new (&_screenshot) ScreenshotHelper(s, false, 0, true, _path);
new (&_screenshot) ScreenshotHelper(s, QRect(), false, 0, true, _path);
obs_source_release(s);
}

View File

@@ -29,6 +29,10 @@ const static std::map<MacroActionSource::Action, std::string> actionTypes = {
"AdvSceneSwitcher.action.source.type.deinterlaceOrder"},
{MacroActionSource::Action::OPEN_INTERACTION_DIALOG,
"AdvSceneSwitcher.action.source.type.openInteractionDialog"},
{MacroActionSource::Action::OPEN_FILTER_DIALOG,
"AdvSceneSwitcher.action.source.type.openFilterDialog"},
{MacroActionSource::Action::OPEN_PROPERTIES_DIALOG,
"AdvSceneSwitcher.action.source.type.openPropertiesDialog"},
};
const static std::map<obs_deinterlace_mode, std::string> deinterlaceModes = {
@@ -60,6 +64,16 @@ const static std::map<obs_deinterlace_field_order, std::string>
"AdvSceneSwitcher.action.source.deinterlaceOrder.bottomFieldFirst"},
};
const static std::map<MacroActionSource::SettingsInputMethod, std::string>
inputMethods = {
{MacroActionSource::SettingsInputMethod::INDIVIDUAL_MANUAL,
"AdvSceneSwitcher.action.source.inputMethod.individualManual"},
{MacroActionSource::SettingsInputMethod::INDIVIDUAL_TEMPVAR,
"AdvSceneSwitcher.action.source.inputMethod.individualTempvar"},
{MacroActionSource::SettingsInputMethod::JSON_STRING,
"AdvSceneSwitcher.action.source.inputMethod.json"},
};
static std::vector<SourceSettingButton> getSourceButtons(OBSWeakSource source)
{
auto s = obs_weak_source_get_source(source);
@@ -75,6 +89,7 @@ static std::vector<SourceSettingButton> getSourceButtons(OBSWeakSource source)
buttons.emplace_back(button);
} while (obs_property_next(&it));
obs_source_release(s);
obs_properties_destroy(sourceProperties);
return buttons;
}
@@ -129,7 +144,26 @@ bool MacroActionSource::PerformAction()
obs_source_set_enabled(s, false);
break;
case Action::SETTINGS:
SetSourceSettings(s, _settings);
switch (_settingsInputMethod) {
case SettingsInputMethod::INDIVIDUAL_MANUAL:
SetSourceSetting(s, _setting, _manualSettingValue);
break;
case SettingsInputMethod::INDIVIDUAL_TEMPVAR: {
auto var = _tempVar.GetTempVariable(GetMacro());
if (!var) {
break;
}
auto value = var->Value();
if (!value) {
break;
}
SetSourceSetting(s, _setting, *value);
break;
}
case SettingsInputMethod::JSON_STRING:
SetSourceSettings(s, _settingsString);
break;
}
break;
case Action::REFRESH_SETTINGS:
refreshSourceSettings(s);
@@ -153,6 +187,12 @@ bool MacroActionSource::PerformAction()
_source.ToString().c_str());
}
break;
case Action::OPEN_FILTER_DIALOG:
obs_frontend_open_source_filters(s);
break;
case Action::OPEN_PROPERTIES_DIALOG:
obs_frontend_open_source_properties(s);
break;
default:
break;
}
@@ -178,7 +218,12 @@ bool MacroActionSource::Save(obs_data_t *obj) const
_source.Save(obj);
obs_data_set_int(obj, "action", static_cast<int>(_action));
_button.Save(obj);
_settings.Save(obj, "settings");
obs_data_set_int(obj, "inputMethod",
static_cast<int>(_settingsInputMethod));
_setting.Save(obj);
_manualSettingValue.Save(obj, "manualSettingValue");
_tempVar.Save(obj);
_settingsString.Save(obj, "settings");
obs_data_set_int(obj, "deinterlaceMode",
static_cast<int>(_deinterlaceMode));
obs_data_set_int(obj, "deinterlaceOrder",
@@ -192,7 +237,17 @@ bool MacroActionSource::Load(obs_data_t *obj)
_source.Load(obj);
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
_button.Load(obj);
_settings.Load(obj, "settings");
// TODO: Remove fallback in future version
if (obs_data_has_user_value(obj, "inputMethod")) {
_settingsInputMethod = static_cast<SettingsInputMethod>(
obs_data_get_int(obj, "inputMethod"));
} else {
_settingsInputMethod = SettingsInputMethod::JSON_STRING;
}
_setting.Load(obj);
_settingsString.Load(obj, "settings");
_manualSettingValue.Load(obj, "manualSettingValue");
_tempVar.Load(obj, GetMacro());
_deinterlaceMode = static_cast<obs_deinterlace_mode>(
obs_data_get_int(obj, "deinterlaceMode"));
_deinterlaceOrder = static_cast<obs_deinterlace_field_order>(
@@ -255,19 +310,34 @@ static inline void populateDeinterlaceFieldOrderSelection(QComboBox *list)
}
}
static inline void populateSettingsInputMethods(QComboBox *list)
{
list->clear();
for (const auto &[value, name] : inputMethods) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(value));
}
}
MacroActionSourceEdit::MacroActionSourceEdit(
QWidget *parent, std::shared_ptr<MacroActionSource> entryData)
: QWidget(parent),
_sources(new SourceSelectionWidget(this, QStringList(), true)),
_actions(new QComboBox()),
_settingsButtons(new QComboBox()),
_settingsLayout(new QHBoxLayout()),
_settingsInputMethods(new QComboBox(this)),
_manualSettingValue(new VariableTextEdit(this, 5, 1, 1)),
_tempVars(new TempVariableSelection(this)),
_sourceSettings(new SourceSettingSelection(this)),
_settingsString(new VariableTextEdit(this)),
_getSettings(new QPushButton(obs_module_text(
"AdvSceneSwitcher.action.source.getSettings"))),
_settings(new VariableTextEdit(this)),
_deinterlaceMode(new QComboBox()),
_deinterlaceOrder(new QComboBox()),
_warning(new QLabel(
obs_module_text("AdvSceneSwitcher.action.source.warning")))
{
populateActionSelection(_actions);
auto sources = GetSourceNames();
@@ -275,6 +345,7 @@ MacroActionSourceEdit::MacroActionSourceEdit(
_sources->SetSourceNameList(sources);
populateDeinterlaceModeSelection(_deinterlaceMode);
populateDeinterlaceFieldOrderSelection(_deinterlaceOrder);
populateSettingsInputMethods(_settingsInputMethods);
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionChanged(int)));
@@ -285,20 +356,33 @@ MacroActionSourceEdit::MacroActionSourceEdit(
SLOT(SourceChanged(const SourceSelection &)));
QWidget::connect(_getSettings, SIGNAL(clicked()), this,
SLOT(GetSettingsClicked()));
QWidget::connect(_settings, SIGNAL(textChanged()), this,
SLOT(SettingsChanged()));
QWidget::connect(_settingsString, SIGNAL(textChanged()), this,
SLOT(SettingsStringChanged()));
QWidget::connect(_deinterlaceMode, SIGNAL(currentIndexChanged(int)),
this, SLOT(DeinterlaceModeChanged(int)));
QWidget::connect(_deinterlaceOrder, SIGNAL(currentIndexChanged(int)),
this, SLOT(DeinterlaceOrderChanged(int)));
QWidget::connect(_tempVars,
SIGNAL(SelectionChanged(const TempVariableRef &)),
this, SLOT(SelectionChanged(const TempVariableRef &)));
QWidget::connect(_settingsInputMethods,
SIGNAL(currentIndexChanged(int)), this,
SLOT(SettingsInputMethodChanged(int)));
QWidget::connect(_manualSettingValue, SIGNAL(textChanged()), this,
SLOT(ManualSettingsValueChanged()));
QWidget::connect(_sourceSettings,
SIGNAL(SelectionChanged(const SourceSetting &)), this,
SLOT(SelectionChanged(const SourceSetting &)));
QVBoxLayout *mainLayout = new QVBoxLayout;
QHBoxLayout *entryLayout = new QHBoxLayout;
QHBoxLayout *buttonLayout = new QHBoxLayout;
auto entryLayout = new QHBoxLayout;
entryLayout->setContentsMargins(0, 0, 0, 0);
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{sources}}", _sources},
{"{{actions}}", _actions},
{"{{settings}}", _settings},
{"{{settings}}", _sourceSettings},
{"{{settingsInputMethod}}", _settingsInputMethods},
{"{{settingValue}}", _manualSettingValue},
{"{{tempVar}}", _tempVars},
{"{{getSettings}}", _getSettings},
{"{{settingsButtons}}", _settingsButtons},
{"{{deinterlaceMode}}", _deinterlaceMode},
@@ -306,9 +390,17 @@ MacroActionSourceEdit::MacroActionSourceEdit(
};
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.source.entry"),
entryLayout, widgetPlaceholders);
_settingsLayout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.source.entry.settings"),
_settingsLayout, widgetPlaceholders);
auto mainLayout = new QVBoxLayout;
mainLayout->addLayout(entryLayout);
mainLayout->addLayout(_settingsLayout);
mainLayout->addWidget(_warning);
mainLayout->addWidget(_settings);
mainLayout->addWidget(_settingsString);
auto buttonLayout = new QHBoxLayout;
buttonLayout->setContentsMargins(0, 0, 0, 0);
buttonLayout->addWidget(_getSettings);
buttonLayout->addStretch();
mainLayout->addLayout(buttonLayout);
@@ -329,13 +421,20 @@ void MacroActionSourceEdit::UpdateEntryData()
_entryData->_source.GetSource());
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_sources->SetSource(_entryData->_source);
_sourceSettings->SetSource(_entryData->_source.GetSource());
_sourceSettings->SetSetting(_entryData->_setting);
_settingsButtons->setCurrentText(
QString::fromStdString(_entryData->_button.ToString()));
_settings->setPlainText(_entryData->_settings);
_settingsString->setPlainText(_entryData->_settingsString);
_deinterlaceMode->setCurrentIndex(_deinterlaceMode->findData(
static_cast<int>(_entryData->_deinterlaceMode)));
_deinterlaceOrder->setCurrentIndex(_deinterlaceOrder->findData(
static_cast<int>(_entryData->_deinterlaceOrder)));
_settingsInputMethods->setCurrentIndex(_settingsInputMethods->findData(
static_cast<int>(_entryData->_settingsInputMethod)));
_tempVars->SetVariable(_entryData->_tempVar);
_manualSettingValue->setPlainText(_entryData->_manualSettingValue);
SetWidgetVisibility();
}
@@ -351,6 +450,7 @@ void MacroActionSourceEdit::SourceChanged(const SourceSelection &source)
}
populateSourceButtonSelection(_settingsButtons,
_entryData->_source.GetSource());
_sourceSettings->SetSource(_entryData->_source.GetSource());
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
@@ -383,18 +483,29 @@ void MacroActionSourceEdit::GetSettingsClicked()
return;
}
_settings->setPlainText(FormatJsonString(
GetSourceSettings(_entryData->_source.GetSource())));
switch (_entryData->_settingsInputMethod) {
case MacroActionSource::SettingsInputMethod::INDIVIDUAL_MANUAL:
_manualSettingValue->setPlainText(GetSourceSettingValue(
_entryData->_source.GetSource(), _entryData->_setting));
break;
case MacroActionSource::SettingsInputMethod::INDIVIDUAL_TEMPVAR:
break;
case MacroActionSource::SettingsInputMethod::JSON_STRING:
_settingsString->setPlainText(FormatJsonString(
GetSourceSettings(_entryData->_source.GetSource())));
break;
}
}
void MacroActionSourceEdit::SettingsChanged()
void MacroActionSourceEdit::SettingsStringChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_settings = _settings->toPlainText().toStdString();
_entryData->_settingsString =
_settingsString->toPlainText().toStdString();
adjustSize();
updateGeometry();
@@ -423,15 +534,87 @@ void MacroActionSourceEdit::DeinterlaceOrderChanged(int idx)
_deinterlaceOrder->itemData(idx).toInt());
}
void MacroActionSourceEdit::SelectionChanged(const TempVariableRef &var)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_tempVar = var;
}
void MacroActionSourceEdit::SettingsInputMethodChanged(int idx)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_settingsInputMethod =
static_cast<MacroActionSource::SettingsInputMethod>(
_settingsInputMethods->itemData(idx).toInt());
SetWidgetVisibility();
}
void MacroActionSourceEdit::SelectionChanged(const SourceSetting &setting)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_setting = setting;
}
void MacroActionSourceEdit::ManualSettingsValueChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_manualSettingValue =
_manualSettingValue->toPlainText().toStdString();
adjustSize();
updateGeometry();
}
void MacroActionSourceEdit::SetWidgetVisibility()
{
const bool showSettings = _entryData->_action ==
MacroActionSource::Action::SETTINGS;
SetLayoutVisible(_settingsLayout,
_entryData->_action ==
MacroActionSource::Action::SETTINGS);
_sourceSettings->setVisible(
_entryData->_action == MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod !=
MacroActionSource::SettingsInputMethod::JSON_STRING);
_settingsString->setVisible(
_entryData->_action == MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::JSON_STRING);
_getSettings->setVisible(_entryData->_action ==
MacroActionSource::Action::SETTINGS);
_tempVars->setVisible(_entryData->_action ==
MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::
INDIVIDUAL_TEMPVAR);
if (_entryData->_action == MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::INDIVIDUAL_MANUAL) {
RemoveStretchIfPresent(_settingsLayout);
_manualSettingValue->show();
} else {
AddStretchIfNecessary(_settingsLayout);
_manualSettingValue->hide();
}
const bool showWarning =
_entryData->_action == MacroActionSource::Action::ENABLE ||
_entryData->_action == MacroActionSource::Action::DISABLE;
_settings->setVisible(showSettings);
_getSettings->setVisible(showSettings);
_warning->setVisible(showWarning);
_settingsButtons->setVisible(
_entryData->_action ==

View File

@@ -2,6 +2,7 @@
#include "macro-action-edit.hpp"
#include "variable-text-edit.hpp"
#include "source-selection.hpp"
#include "source-setting.hpp"
#include <QSpinBox>
#include <QLabel>
@@ -35,10 +36,13 @@ public:
SourceSelection _source;
SourceSettingButton _button;
StringVariable _settings = "";
StringVariable _settingsString = "";
StringVariable _manualSettingValue = "";
obs_deinterlace_mode _deinterlaceMode = OBS_DEINTERLACE_MODE_DISABLE;
obs_deinterlace_field_order _deinterlaceOrder =
OBS_DEINTERLACE_FIELD_ORDER_TOP;
TempVariableRef _tempVar;
SourceSetting _setting;
enum class Action {
ENABLE,
@@ -49,8 +53,18 @@ public:
DEINTERLACE_MODE,
DEINTERLACE_FIELD_ORDER,
OPEN_INTERACTION_DIALOG,
OPEN_FILTER_DIALOG,
OPEN_PROPERTIES_DIALOG,
};
Action _action = Action::ENABLE;
Action _action = Action::SETTINGS;
enum class SettingsInputMethod {
INDIVIDUAL_MANUAL,
INDIVIDUAL_TEMPVAR,
JSON_STRING,
};
SettingsInputMethod _settingsInputMethod =
SettingsInputMethod::INDIVIDUAL_MANUAL;
private:
static bool _registered;
@@ -78,9 +92,13 @@ private slots:
void ActionChanged(int value);
void ButtonChanged(int idx);
void GetSettingsClicked();
void SettingsChanged();
void SettingsStringChanged();
void DeinterlaceModeChanged(int);
void DeinterlaceOrderChanged(int);
void SelectionChanged(const TempVariableRef &);
void SettingsInputMethodChanged(int);
void SelectionChanged(const SourceSetting &);
void ManualSettingsValueChanged();
signals:
void HeaderInfoChanged(const QString &);
@@ -88,11 +106,17 @@ protected:
SourceSelectionWidget *_sources;
QComboBox *_actions;
QComboBox *_settingsButtons;
QHBoxLayout *_settingsLayout;
QComboBox *_settingsInputMethods;
VariableTextEdit *_manualSettingValue;
TempVariableSelection *_tempVars;
SourceSettingSelection *_sourceSettings;
VariableTextEdit *_settingsString;
QPushButton *_getSettings;
VariableTextEdit *_settings;
QComboBox *_deinterlaceMode;
QComboBox *_deinterlaceOrder;
QLabel *_warning;
std::shared_ptr<MacroActionSource> _entryData;
private:

View File

@@ -45,6 +45,12 @@ const static std::map<MacroActionVariable::Type, std::string> actionTypes = {
"AdvSceneSwitcher.action.variable.type.environmentVariable"},
{MacroActionVariable::Type::SCENE_ITEM_COUNT,
"AdvSceneSwitcher.action.variable.type.sceneItemCount"},
{MacroActionVariable::Type::STRING_LENGTH,
"AdvSceneSwitcher.action.variable.type.stringLength"},
{MacroActionVariable::Type::EXTRACT_JSON,
"AdvSceneSwitcher.action.variable.type.extractJson"},
{MacroActionVariable::Type::SET_TO_TEMPVAR,
"AdvSceneSwitcher.action.variable.type.setToTempvar"},
};
static void apppend(Variable &var, const std::string &value)
@@ -238,6 +244,30 @@ bool MacroActionVariable::PerformAction()
var->SetValue(GetSceneItemCount(_scene.GetScene(false)));
return true;
}
case Type::STRING_LENGTH: {
var->SetValue(std::string(_strValue).length());
return true;
}
case Type::EXTRACT_JSON: {
auto value = GetJsonField(var->Value(), _strValue);
if (!value.has_value()) {
return true;
}
var->SetValue(*value);
return true;
}
case Type::SET_TO_TEMPVAR: {
auto tempVar = _tempVar.GetTempVariable(GetMacro());
if (!tempVar) {
return true;
}
auto value = tempVar->Value();
if (!value) {
return true;
}
var->SetValue(*value);
return true;
}
}
return true;
@@ -250,7 +280,7 @@ bool MacroActionVariable::Save(obs_data_t *obj) const
GetWeakVariableName(_variable).c_str());
obs_data_set_string(obj, "variable2Name",
GetWeakVariableName(_variable2).c_str());
obs_data_set_string(obj, "strValue", _strValue.c_str());
_strValue.Save(obj, "strValue");
obs_data_set_double(obj, "numValue", _numValue);
obs_data_set_int(obj, "condition", static_cast<int>(_type));
obs_data_set_int(obj, "segmentIdx", GetSegmentIndexValue());
@@ -258,8 +288,8 @@ bool MacroActionVariable::Save(obs_data_t *obj) const
obs_data_set_int(obj, "subStringSize", _subStringSize);
obs_data_set_string(obj, "regexPattern", _regexPattern.c_str());
obs_data_set_int(obj, "regexMatchIdx", _regexMatchIdx);
obs_data_set_string(obj, "findStr", _findStr.c_str());
obs_data_set_string(obj, "replaceStr", _replaceStr.c_str());
_findStr.Save(obj, "findStr");
_replaceStr.Save(obj, "replaceStr");
_regex.Save(obj);
_mathExpression.Save(obj, "mathExpression");
obs_data_set_bool(obj, "useCustomPrompt", _useCustomPrompt);
@@ -268,6 +298,7 @@ bool MacroActionVariable::Save(obs_data_t *obj) const
_inputPlaceholder.Save(obj, "inputPlaceholder");
_envVariableName.Save(obj, "environmentVariableName");
_scene.Save(obj);
_tempVar.Save(obj);
return true;
}
@@ -278,7 +309,7 @@ bool MacroActionVariable::Load(obs_data_t *obj)
GetWeakVariableByName(obs_data_get_string(obj, "variableName"));
_variable2 = GetWeakVariableByName(
obs_data_get_string(obj, "variable2Name"));
_strValue = obs_data_get_string(obj, "strValue");
_strValue.Load(obj, "strValue");
_numValue = obs_data_get_double(obj, "numValue");
_type = static_cast<Type>(obs_data_get_int(obj, "condition"));
_segmentIdxLoadValue = obs_data_get_int(obj, "segmentIdx");
@@ -287,8 +318,8 @@ bool MacroActionVariable::Load(obs_data_t *obj)
_regex.Load(obj);
_regexPattern = obs_data_get_string(obj, "regexPattern");
_regexMatchIdx = obs_data_get_int(obj, "regexMatchIdx");
_findStr = obs_data_get_string(obj, "findStr");
_replaceStr = obs_data_get_string(obj, "replaceStr");
_findStr.Load(obj, "findStr");
_replaceStr.Load(obj, "replaceStr");
_mathExpression.Load(obj, "mathExpression");
_useCustomPrompt = obs_data_get_bool(obj, "useCustomPrompt");
_inputPrompt.Load(obj, "inputPrompt");
@@ -296,6 +327,7 @@ bool MacroActionVariable::Load(obs_data_t *obj)
_inputPlaceholder.Load(obj, "inputPlaceholder");
_envVariableName.Load(obj, "environmentVariableName");
_scene.Load(obj);
_tempVar.Load(obj, GetMacro());
return true;
}
@@ -338,7 +370,7 @@ void MacroActionVariable::SetSegmentIndexValue(int value)
_macroSegment = segment;
if (segment) {
segment->IncrementVariableRef();
IncrementVariableRef(segment.get());
}
}
@@ -380,13 +412,14 @@ void MacroActionVariable::DecrementCurrentSegmentVariableRef()
return;
}
segment->DecrementVariableRef();
DecrementVariableRef(segment.get());
}
static inline void populateTypeSelection(QComboBox *list)
{
for (auto entry : actionTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
for (const auto &[action, name] : actionTypes) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(action));
}
}
@@ -395,8 +428,8 @@ MacroActionVariableEdit::MacroActionVariableEdit(
: QWidget(parent),
_variables(new VariableSelection(this)),
_variables2(new VariableSelection(this)),
_actions(new QComboBox()),
_strValue(new ResizingPlainTextEdit(this, 5, 1, 1)),
_actions(new FilterComboBox(this)),
_strValue(new VariableTextEdit(this, 5, 1, 1)),
_numValue(new QDoubleSpinBox()),
_segmentIdx(new MacroSegmentSelection(
this, MacroSegmentSelection::Type::CONDITION, false)),
@@ -411,8 +444,8 @@ MacroActionVariableEdit::MacroActionVariableEdit(
_regexPattern(new ResizingPlainTextEdit(this, 10, 1, 1)),
_regexMatchIdx(new QSpinBox()),
_findReplaceLayout(new QHBoxLayout()),
_findStr(new ResizingPlainTextEdit(this, 10, 1, 1)),
_replaceStr(new ResizingPlainTextEdit(this, 10, 1, 1)),
_findStr(new VariableTextEdit(this, 10, 1, 1)),
_replaceStr(new VariableTextEdit(this, 10, 1, 1)),
_mathExpression(new VariableLineEdit(this)),
_mathExpressionResult(new QLabel()),
_promptLayout(new QHBoxLayout()),
@@ -422,7 +455,9 @@ MacroActionVariableEdit::MacroActionVariableEdit(
_useInputPlaceholder(new QCheckBox()),
_inputPlaceholder(new VariableLineEdit(this)),
_envVariable(new VariableLineEdit(this)),
_scenes(new SceneSelectionWidget(this, true, false, true, true, true))
_scenes(new SceneSelectionWidget(this, true, false, true, true,
true)),
_tempVars(new TempVariableSelection(this))
{
_numValue->setMinimum(-9999999999);
_numValue->setMaximum(9999999999);
@@ -485,6 +520,9 @@ MacroActionVariableEdit::MacroActionVariableEdit(
SLOT(EnvVariableChanged()));
QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
this, SLOT(SceneChanged(const SceneSelection &)));
QWidget::connect(_tempVars,
SIGNAL(SelectionChanged(const TempVariableRef &)),
this, SLOT(SelectionChanged(const TempVariableRef &)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{variables}}", _variables},
@@ -505,6 +543,7 @@ MacroActionVariableEdit::MacroActionVariableEdit(
{"{{inputPlaceholder}}", _inputPlaceholder},
{"{{envVariableName}}", _envVariable},
{"{{scenes}}", _scenes},
{"{{tempVars}}", _tempVars},
};
auto entryLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.variable.entry"),
@@ -572,8 +611,9 @@ void MacroActionVariableEdit::UpdateEntryData()
_variables->SetVariable(_entryData->_variable);
_variables2->SetVariable(_entryData->_variable2);
_actions->setCurrentIndex(static_cast<int>(_entryData->_type));
_strValue->setPlainText(QString::fromStdString(_entryData->_strValue));
_actions->setCurrentIndex(
_actions->findData(static_cast<int>(_entryData->_type)));
_strValue->setPlainText(_entryData->_strValue);
_numValue->setValue(_entryData->_numValue);
_segmentIdx->SetValue(_entryData->GetSegmentIndexValue() + 1);
_segmentIdx->SetMacro(_entryData->GetMacro());
@@ -588,9 +628,8 @@ void MacroActionVariableEdit::UpdateEntryData()
_regexPattern->setPlainText(
QString::fromStdString(_entryData->_regexPattern));
_regexMatchIdx->setValue(_entryData->_regexMatchIdx + 1);
_findStr->setPlainText(QString::fromStdString(_entryData->_findStr));
_replaceStr->setPlainText(
QString::fromStdString(_entryData->_replaceStr));
_findStr->setPlainText(_entryData->_findStr);
_replaceStr->setPlainText(_entryData->_replaceStr);
_mathExpression->setText(_entryData->_mathExpression);
_useCustomPrompt->setChecked(_entryData->_useCustomPrompt);
_inputPrompt->setText(_entryData->_inputPrompt);
@@ -598,6 +637,7 @@ void MacroActionVariableEdit::UpdateEntryData()
_inputPlaceholder->setText(_entryData->_inputPlaceholder);
_envVariable->setText(_entryData->_envVariableName);
_scenes->SetScene(_entryData->_scene);
_tempVars->SetVariable(_entryData->_tempVar);
SetWidgetVisibility();
}
@@ -621,14 +661,15 @@ void MacroActionVariableEdit::Variable2Changed(const QString &text)
_entryData->_variable2 = GetWeakVariableByQString(text);
}
void MacroActionVariableEdit::ActionChanged(int value)
void MacroActionVariableEdit::ActionChanged(int idx)
{
if (_loading || !_entryData) {
if (_loading || !_entryData || idx == -1) {
return;
}
auto lock = LockContext();
_entryData->_type = static_cast<MacroActionVariable::Type>(value);
_entryData->_type = static_cast<MacroActionVariable::Type>(
_actions->itemData(idx).toInt());
if (_entryData->_type == MacroActionVariable::Type::SET_ACTION_VALUE) {
_segmentIdx->SetType(MacroSegmentSelection::Type::ACTION);
@@ -725,7 +766,7 @@ void MacroActionVariableEdit::UpdateSegmentVariableValue()
return;
}
if (!segment->SupportsVariableValue()) {
if (!SupportsVariableValue(segment.get())) {
std::string type;
QString fmt;
@@ -750,8 +791,14 @@ void MacroActionVariableEdit::UpdateSegmentVariableValue()
_segmentValueStatus->setText(obs_module_text(
"AdvSceneSwitcher.action.variable.currentSegmentValue"));
_segmentValue->show();
_segmentValue->setPlainText(
QString::fromStdString(segment->GetVariableValue()));
// Only update the text if the value changed to prevent possible text
// selections being lost
auto previousText = _segmentValue->toPlainText();
auto newText = QString::fromStdString(segment->GetVariableValue());
if (newText != previousText) {
_segmentValue->setPlainText(newText);
}
adjustSize();
updateGeometry();
@@ -925,6 +972,16 @@ void MacroActionVariableEdit::SceneChanged(const SceneSelection &scene)
_entryData->_scene = scene;
}
void MacroActionVariableEdit::SelectionChanged(const TempVariableRef &var)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_tempVar = var;
}
void MacroActionVariableEdit::SetWidgetVisibility()
{
if (!_entryData) {
@@ -936,7 +993,9 @@ void MacroActionVariableEdit::SetWidgetVisibility()
_strValue->setVisible(
_entryData->_type ==
MacroActionVariable::Type::SET_FIXED_VALUE ||
_entryData->_type == MacroActionVariable::Type::APPEND);
_entryData->_type == MacroActionVariable::Type::APPEND ||
_entryData->_type == MacroActionVariable::Type::STRING_LENGTH ||
_entryData->_type == MacroActionVariable::Type::EXTRACT_JSON);
_numValue->setVisible(
_entryData->_type == MacroActionVariable::Type::INCREMENT ||
_entryData->_type == MacroActionVariable::Type::DECREMENT);
@@ -1001,6 +1060,8 @@ void MacroActionVariableEdit::SetWidgetVisibility()
MacroActionVariable::Type::ENV_VARIABLE);
_scenes->setVisible(_entryData->_type ==
MacroActionVariable::Type::SCENE_ITEM_COUNT);
_tempVars->setVisible(_entryData->_type ==
MacroActionVariable::Type::SET_TO_TEMPVAR);
adjustSize();
updateGeometry();
}

View File

@@ -5,6 +5,7 @@
#include "resizing-text-edit.hpp"
#include "scene-selection.hpp"
#include "variable-line-edit.hpp"
#include "variable-text-edit.hpp"
namespace advss {
@@ -40,21 +41,24 @@ public:
USER_INPUT,
ENV_VARIABLE,
SCENE_ITEM_COUNT,
STRING_LENGTH,
EXTRACT_JSON,
SET_TO_TEMPVAR,
};
Type _type = Type::SET_FIXED_VALUE;
std::weak_ptr<Variable> _variable;
std::weak_ptr<Variable> _variable2;
std::string _strValue = "";
StringVariable _strValue = "";
double _numValue = 0;
int _subStringStart = 0;
int _subStringSize = 0;
RegexConfig _regex = RegexConfig::PartialMatchRegexConfig();
std::string _regexPattern = ".*";
int _regexMatchIdx = 0;
std::string _findStr = obs_module_text(
StringVariable _findStr = obs_module_text(
"AdvSceneSwitcher.action.variable.findAndReplace.find");
std::string _replaceStr = obs_module_text(
StringVariable _replaceStr = obs_module_text(
"AdvSceneSwitcher.action.variable.findAndReplace.replace");
StringVariable _mathExpression = obs_module_text(
"AdvSceneSwitcher.action.variable.mathExpression.example");
@@ -70,6 +74,7 @@ public:
StringVariable _envVariableName = "HOME";
#endif
SceneSelection _scene;
TempVariableRef _tempVar;
private:
void DecrementCurrentSegmentVariableRef();
@@ -123,6 +128,7 @@ private slots:
void InputPlaceholderChanged();
void EnvVariableChanged();
void SceneChanged(const SceneSelection &);
void SelectionChanged(const TempVariableRef &var);
signals:
void HeaderInfoChanged(const QString &);
@@ -133,8 +139,8 @@ private:
VariableSelection *_variables;
VariableSelection *_variables2;
QComboBox *_actions;
ResizingPlainTextEdit *_strValue;
FilterComboBox *_actions;
VariableTextEdit *_strValue;
QDoubleSpinBox *_numValue;
MacroSegmentSelection *_segmentIdx;
QLabel *_segmentValueStatus;
@@ -148,8 +154,8 @@ private:
ResizingPlainTextEdit *_regexPattern;
QSpinBox *_regexMatchIdx;
QHBoxLayout *_findReplaceLayout;
ResizingPlainTextEdit *_findStr;
ResizingPlainTextEdit *_replaceStr;
VariableTextEdit *_findStr;
VariableTextEdit *_replaceStr;
VariableLineEdit *_mathExpression;
QLabel *_mathExpressionResult;
QHBoxLayout *_promptLayout;
@@ -160,6 +166,7 @@ private:
VariableLineEdit *_inputPlaceholder;
VariableLineEdit *_envVariable;
SceneSelectionWidget *_scenes;
TempVariableSelection *_tempVars;
std::shared_ptr<MacroActionVariable> _entryData;
QTimer _timer;

View File

@@ -192,7 +192,7 @@ void MacroActionWebsocketEdit::CheckForSettingsConflict()
updateGeometry();
}
void MacroActionWebsocketEdit::SetupWidgetVisibility()
void MacroActionWebsocketEdit::SetWidgetVisibility()
{
_messageType->setVisible(_entryData->_api ==
MacroActionWebsocket::API::SCENE_SWITCHER);
@@ -283,7 +283,7 @@ void MacroActionWebsocketEdit::UpdateEntryData()
_message->setPlainText(_entryData->_message);
_connection->SetConnection(_entryData->_connection);
SetupWidgetVisibility();
SetWidgetVisibility();
}
void MacroActionWebsocketEdit::APITypeChanged(int index)
@@ -294,7 +294,7 @@ void MacroActionWebsocketEdit::APITypeChanged(int index)
auto lock = LockContext();
_entryData->_api = static_cast<MacroActionWebsocket::API>(index);
SetupWidgetVisibility();
SetWidgetVisibility();
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
@@ -308,7 +308,7 @@ void MacroActionWebsocketEdit::MessageTypeChanged(int index)
auto lock = LockContext();
_entryData->_type =
static_cast<MacroActionWebsocket::MessageType>(index);
SetupWidgetVisibility();
SetWidgetVisibility();
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}

View File

@@ -76,7 +76,7 @@ protected:
private:
void CheckForSettingsConflict();
void SetupWidgetVisibility();
void SetWidgetVisibility();
void SetupRequestEdit();
void SetupEventEdit();
void SetupGenericEdit();

View File

@@ -47,6 +47,12 @@ const static std::map<MacroConditionAudio::VolumeCondition, std::string>
"AdvSceneSwitcher.condition.audio.state.unmute"},
};
void MacroConditionAudio::SetType(const Type &type)
{
_checkType = type;
SetupTempVars();
}
MacroConditionAudio::~MacroConditionAudio()
{
obs_volmeter_remove_callback(_volmeter, SetVolumeLevel, this);
@@ -74,6 +80,7 @@ bool MacroConditionAudio::CheckOutputCondition()
}
SetVariableValue(std::to_string(curVolume));
SetTempVarValue("output_volume", std::to_string(curVolume));
// Reset for next check
_peak = -std::numeric_limits<float>::infinity();
@@ -118,6 +125,9 @@ bool MacroConditionAudio::CheckVolumeCondition()
break;
}
SetTempVarValue("configured_volume", std::to_string(curVolume));
SetTempVarValue("muted", muted ? "true" : "false");
obs_source_release(s);
return ret;
}
@@ -137,6 +147,7 @@ bool MacroConditionAudio::CheckSyncOffset()
ret = curOffset < _syncOffset;
}
SetVariableValue(std::to_string(curOffset));
SetTempVarValue("sync_offset", std::to_string(curOffset));
obs_source_release(s);
return ret;
}
@@ -151,6 +162,8 @@ bool MacroConditionAudio::CheckMonitor()
auto s = obs_weak_source_get_source(_audioSource.GetSource());
ret = obs_source_get_monitoring_type(s) == _monitorType;
SetVariableValue("");
SetTempVarValue("monitor",
std::to_string(obs_source_get_monitoring_type(s)));
obs_source_release(s);
return ret;
}
@@ -170,6 +183,7 @@ bool MacroConditionAudio::CheckBalance()
ret = curBalance < _balance;
}
SetVariableValue(std::to_string(curBalance));
SetTempVarValue("balance", std::to_string(curBalance));
obs_source_release(s);
return ret;
}
@@ -290,6 +304,50 @@ void MacroConditionAudio::ResetVolmeter()
_volmeter = AddVolmeterToSource(this, _audioSource.GetSource());
}
void MacroConditionAudio::SetupTempVars()
{
MacroCondition::SetupTempVars();
switch (_checkType) {
case Type::OUTPUT_VOLUME:
AddTempvar(
"output_volume",
obs_module_text(
"AdvSceneSwitcher.tempVar.audio.output_volume"),
obs_module_text(
"AdvSceneSwitcher.tempVar.audio.output_volume.description"));
break;
case Type::CONFIGURED_VOLUME:
AddTempvar(
"configured_volume",
obs_module_text(
"AdvSceneSwitcher.tempVar.audio.configured_volume"),
obs_module_text(
"AdvSceneSwitcher.tempVar.audio.configured_volume.description"));
AddTempvar("muted",
obs_module_text(
"AdvSceneSwitcher.tempVar.audio.muted"));
break;
case Type::SYNC_OFFSET:
AddTempvar(
"sync_offset",
obs_module_text(
"AdvSceneSwitcher.tempVar.audio.sync_offset"));
break;
case Type::MONITOR:
AddTempvar("monitor",
obs_module_text(
"AdvSceneSwitcher.tempVar.audio.monitor"));
break;
case Type::BALANCE:
AddTempvar("balance",
obs_module_text(
"AdvSceneSwitcher.tempVar.audio.balance"));
break;
default:
break;
}
}
static inline void populateCheckTypes(QComboBox *list)
{
list->clear();
@@ -479,10 +537,9 @@ void MacroConditionAudioEdit::ConditionChanged(int cond)
}
auto lock = LockContext();
if (_entryData->_checkType ==
MacroConditionAudio::Type::OUTPUT_VOLUME ||
_entryData->_checkType == MacroConditionAudio::Type::BALANCE ||
_entryData->_checkType == MacroConditionAudio::Type::SYNC_OFFSET) {
if (_entryData->GetType() == MacroConditionAudio::Type::OUTPUT_VOLUME ||
_entryData->GetType() == MacroConditionAudio::Type::BALANCE ||
_entryData->GetType() == MacroConditionAudio::Type::SYNC_OFFSET) {
_entryData->_outputCondition =
static_cast<MacroConditionAudio::OutputCondition>(cond);
} else {
@@ -499,16 +556,15 @@ void MacroConditionAudioEdit::CheckTypeChanged(int idx)
}
auto lock = LockContext();
_entryData->_checkType = static_cast<MacroConditionAudio::Type>(
_checkTypes->itemData(idx).toInt());
_entryData->SetType(static_cast<MacroConditionAudio::Type>(
_checkTypes->itemData(idx).toInt()));
const QSignalBlocker b(_condition);
if (_entryData->_checkType ==
MacroConditionAudio::Type::OUTPUT_VOLUME ||
_entryData->_checkType == MacroConditionAudio::Type::BALANCE ||
_entryData->_checkType == MacroConditionAudio::Type::SYNC_OFFSET) {
if (_entryData->GetType() == MacroConditionAudio::Type::OUTPUT_VOLUME ||
_entryData->GetType() == MacroConditionAudio::Type::BALANCE ||
_entryData->GetType() == MacroConditionAudio::Type::SYNC_OFFSET) {
populateOutputConditionSelection(_condition);
} else if (_entryData->_checkType ==
} else if (_entryData->GetType() ==
MacroConditionAudio::Type::CONFIGURED_VOLUME) {
populateVolumeConditionSelection(_condition);
}
@@ -526,17 +582,16 @@ void MacroConditionAudioEdit::UpdateEntryData()
_syncOffset->SetValue(_entryData->_syncOffset);
_monitorTypes->setCurrentIndex(_entryData->_monitorType);
_balance->SetDoubleValue(_entryData->_balance);
_checkTypes->setCurrentIndex(_checkTypes->findData(
static_cast<int>(_entryData->_checkType)));
_checkTypes->setCurrentIndex(
_checkTypes->findData(static_cast<int>(_entryData->GetType())));
if (_entryData->_checkType ==
MacroConditionAudio::Type::OUTPUT_VOLUME ||
_entryData->_checkType == MacroConditionAudio::Type::BALANCE ||
_entryData->_checkType == MacroConditionAudio::Type::SYNC_OFFSET) {
if (_entryData->GetType() == MacroConditionAudio::Type::OUTPUT_VOLUME ||
_entryData->GetType() == MacroConditionAudio::Type::BALANCE ||
_entryData->GetType() == MacroConditionAudio::Type::SYNC_OFFSET) {
populateOutputConditionSelection(_condition);
_condition->setCurrentIndex(
static_cast<int>(_entryData->_outputCondition));
} else if (_entryData->_checkType ==
} else if (_entryData->GetType() ==
MacroConditionAudio::Type::CONFIGURED_VOLUME) {
populateVolumeConditionSelection(_condition);
_condition->setCurrentIndex(
@@ -554,9 +609,9 @@ void MacroConditionAudioEdit::SetWidgetVisibility()
}
_volume->setVisible(
_entryData->_checkType ==
_entryData->GetType() ==
MacroConditionAudio::Type::OUTPUT_VOLUME ||
(_entryData->_checkType ==
(_entryData->GetType() ==
MacroConditionAudio::Type::CONFIGURED_VOLUME &&
(_entryData->_volumeCondition ==
MacroConditionAudio::VolumeCondition::ABOVE ||
@@ -565,20 +620,20 @@ void MacroConditionAudioEdit::SetWidgetVisibility()
_entryData->_volumeCondition ==
MacroConditionAudio::VolumeCondition::BELOW)));
_condition->setVisible(
_entryData->_checkType ==
_entryData->GetType() ==
MacroConditionAudio::Type::OUTPUT_VOLUME ||
_entryData->_checkType ==
_entryData->GetType() ==
MacroConditionAudio::Type::CONFIGURED_VOLUME ||
_entryData->_checkType == MacroConditionAudio::Type::BALANCE ||
_entryData->_checkType ==
_entryData->GetType() == MacroConditionAudio::Type::BALANCE ||
_entryData->GetType() ==
MacroConditionAudio::Type::SYNC_OFFSET);
_syncOffset->setVisible(_entryData->_checkType ==
_syncOffset->setVisible(_entryData->GetType() ==
MacroConditionAudio::Type::SYNC_OFFSET);
_monitorTypes->setVisible(_entryData->_checkType ==
_monitorTypes->setVisible(_entryData->GetType() ==
MacroConditionAudio::Type::MONITOR);
_balance->setVisible(_entryData->_checkType ==
_balance->setVisible(_entryData->GetType() ==
MacroConditionAudio::Type::BALANCE);
_volMeter->setVisible(_entryData->_checkType ==
_volMeter->setVisible(_entryData->GetType() ==
MacroConditionAudio::Type::OUTPUT_VOLUME);
adjustSize();
}

View File

@@ -37,6 +37,8 @@ public:
MONITOR,
BALANCE,
};
void SetType(const Type &);
Type GetType() const { return _checkType; }
enum class OutputCondition {
ABOVE,
@@ -56,7 +58,6 @@ public:
NumberVariable<int> _syncOffset = 0;
obs_monitoring_type _monitorType = OBS_MONITORING_TYPE_NONE;
NumberVariable<double> _balance = 0.5;
Type _checkType = Type::OUTPUT_VOLUME;
OutputCondition _outputCondition = OutputCondition::ABOVE;
VolumeCondition _volumeCondition = VolumeCondition::ABOVE;
obs_volmeter_t *_volmeter = nullptr;
@@ -67,7 +68,9 @@ private:
bool CheckSyncOffset();
bool CheckMonitor();
bool CheckBalance();
void SetupTempVars();
Type _checkType = Type::OUTPUT_VOLUME;
float _peak = -std::numeric_limits<float>::infinity();
static bool _registered;
static const std::string id;

View File

@@ -60,7 +60,8 @@ const static std::map<MacroConditionDate::Day, std::string> dayOfWeekNames = {
bool MacroConditionDate::CheckDayOfWeek(int64_t msSinceLastCheck)
{
QDateTime cur = QDateTime::currentDateTime();
SetVariableValue(cur.toString().toStdString());
SetVariables(cur);
if (_dayOfWeek != Day::ANY &&
cur.date().dayOfWeek() != static_cast<int>(_dayOfWeek)) {
return false;
@@ -133,7 +134,7 @@ bool MacroConditionDate::CheckRegularDate(int64_t msSinceLastCheck)
{
bool match = false;
QDateTime cur = QDateTime::currentDateTime();
SetVariableValue(cur.toString().toStdString());
SetVariables(cur);
if (_ignoreDate) {
_dateTime.setDate(cur.date());
@@ -304,6 +305,37 @@ QDateTime MacroConditionDate::GetNextMatchDateTime() const
return _dateTime;
}
void MacroConditionDate::SetVariables(const QDateTime &date)
{
SetVariableValue(date.toString().toStdString());
SetTempVarValue("year", std::to_string(date.date().year()));
SetTempVarValue("month", std::to_string(date.date().month()));
SetTempVarValue("day", std::to_string(date.date().day()));
SetTempVarValue("hour", std::to_string(date.time().hour()));
SetTempVarValue("minute", std::to_string(date.time().minute()));
SetTempVarValue("second", std::to_string(date.time().second()));
SetTempVarValue("dayOfWeek", std::to_string(date.date().dayOfWeek()));
}
void MacroConditionDate::SetupTempVars()
{
MacroCondition::SetupTempVars();
AddTempvar("year",
obs_module_text("AdvSceneSwitcher.tempVar.date.year"));
AddTempvar("month",
obs_module_text("AdvSceneSwitcher.tempVar.date.month"));
AddTempvar("day", obs_module_text("AdvSceneSwitcher.tempVar.date.day"));
AddTempvar("hour",
obs_module_text("AdvSceneSwitcher.tempVar.date.hour"));
AddTempvar("minute",
obs_module_text("AdvSceneSwitcher.tempVar.date.minute"));
AddTempvar("second",
obs_module_text("AdvSceneSwitcher.tempVar.date.second"));
AddTempvar("dayOfWeek",
obs_module_text("AdvSceneSwitcher.tempVar.date.dayOfWeek"));
}
static inline void populateDaySelection(QComboBox *list)
{
for (auto entry : dayOfWeekNames) {

View File

@@ -64,6 +64,8 @@ private:
bool CheckRegularDate(int64_t);
bool CheckBetween(const QDateTime &now);
bool CheckPattern(QDateTime now, int64_t secondsSinceLastCheck);
void SetVariables(const QDateTime &date);
void SetupTempVars();
QDateTime _dateTime = QDateTime::currentDateTime();
QDateTime _dateTime2 = QDateTime::currentDateTime();

View File

@@ -312,6 +312,7 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text)
*_entryData = MacroConditionFactory::Create(id, macro);
(*_entryData)->SetIndex(idx);
(*_entryData)->SetLogicType(logic);
(*_entryData)->PostLoad();
}
auto widget =
MacroConditionFactory::CreateWidget(id, this, *_entryData);
@@ -342,7 +343,7 @@ void MacroConditionEdit::DurationModifierChanged(DurationModifier::Type m)
(*_entryData)->SetDurationModifier(m);
}
std::shared_ptr<MacroSegment> MacroConditionEdit::Data()
std::shared_ptr<MacroSegment> MacroConditionEdit::Data() const
{
return *_entryData;
}
@@ -383,6 +384,7 @@ void AdvSceneSwitcher::AddMacroCondition(int idx)
macro->Conditions().at(idx)->Load(data);
obs_data_release(data);
}
macro->Conditions().at(idx)->PostLoad();
(*cond)->SetLogicType(logic);
macro->UpdateConditionIndices();
ui->conditionsList->Insert(

View File

@@ -75,7 +75,7 @@ private slots:
private:
void SetLogicSelection();
std::shared_ptr<MacroSegment> Data();
std::shared_ptr<MacroSegment> Data() const;
QComboBox *_logicSelection;
FilterComboBox *_conditionSelection;

View File

@@ -5,7 +5,8 @@
#include <QTextStream>
#include <QFileDialog>
#include <regex>
#include <array>
#include <tuple>
namespace advss {
@@ -41,86 +42,63 @@ static std::string getRemoteData(std::string &url)
return readBuffer;
}
bool MacroConditionFile::MatchFileContent(QString &filedata)
static bool stringsMatch(const QString &text, const std::string &pattern,
const RegexConfig &regex)
{
if (_onlyMatchIfChanged) {
size_t newHash = strHash(filedata.toUtf8().constData());
if (newHash == _lastHash) {
return false;
}
_lastHash = newHash;
auto qpattern = QString::fromStdString(pattern);
if (regex.Enabled()) {
return regex.Matches(text, qpattern);
}
if (_regex.Enabled()) {
auto expr = _regex.GetRegularExpression(_text);
if (!expr.isValid()) {
return false;
}
auto match = expr.match(filedata);
return match.hasMatch();
}
QString text = QString::fromStdString(_text);
return CompareIgnoringLineEnding(text, filedata);
auto textCopy = text;
return CompareIgnoringLineEnding(qpattern, textCopy);
}
bool MacroConditionFile::CheckRemoteFileContent()
static std::optional<QString> getFileContent(MacroConditionFile::FileType type,
const std::string &filePath)
{
std::string path = _file;
std::string data = getRemoteData(path);
SetVariableValue(data);
QString qdata = QString::fromStdString(data);
return MatchFileContent(qdata);
QString filedata;
switch (type) {
case MacroConditionFile::FileType::LOCAL: {
std::string path = filePath;
QFile file(QString::fromStdString(path));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return {};
}
filedata = QTextStream(&file).readAll();
file.close();
} break;
case MacroConditionFile::FileType::REMOTE: {
std::string path = filePath;
std::string data = getRemoteData(path);
filedata = QString::fromStdString(data);
} break;
default:
break;
}
return filedata;
}
bool MacroConditionFile::CheckLocalFileContent()
bool MacroConditionFile::CheckFileContentMatch()
{
QFile file(QString::fromStdString(_file));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
auto fileData = getFileContent(_fileType, _file);
if (!fileData) {
return false;
}
if (_useTime) {
QDateTime newLastMod = QFileInfo(file).lastModified();
if (_lastMod == newLastMod) {
return false;
}
_lastMod = newLastMod;
}
QString filedata = QTextStream(&file).readAll();
SetVariableValue(filedata.toStdString());
bool match = MatchFileContent(filedata);
file.close();
const bool match = stringsMatch(*fileData, _text, _regex);
SetVariableValue(fileData->toStdString());
return match;
}
bool MacroConditionFile::CheckChangeContent()
{
QString filedata;
switch (_fileType) {
case FileType::LOCAL: {
std::string path = _file;
QFile file(QString::fromStdString(path));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
filedata = QTextStream(&file).readAll();
file.close();
} break;
case FileType::REMOTE: {
std::string path = _file;
std::string data = getRemoteData(path);
QString filedata = QString::fromStdString(data);
} break;
default:
break;
auto filedata = getFileContent(_fileType, _file);
if (!filedata) {
return false;
}
size_t newHash = strHash(filedata.toUtf8().constData());
size_t newHash = strHash(filedata->toUtf8().constData());
const bool contentChanged = newHash != _lastHash;
_lastHash = newHash;
SetVariableValue(contentChanged ? "true" : "false");
return contentChanged;
}
@@ -135,35 +113,94 @@ bool MacroConditionFile::CheckChangeDate()
SetVariableValue(newLastMod.toString().toStdString());
const bool dateChanged = _lastMod != newLastMod;
_lastMod = newLastMod;
SetVariableValue(dateChanged ? "true" : "false");
return dateChanged;
}
static QStringList splitLines(const QString &string)
{
static auto regex = QRegularExpression("\n|\r\n|\r");
return string.split(regex);
}
bool MacroConditionFile::CheckChangedMatch()
{
static bool setupDone = false;
if (!setupDone) {
auto content = getFileContent(_fileType, _file);
if (!content) {
return false;
}
_previousContent = splitLines(*content);
setupDone = true;
return false;
}
auto currentFileContent = getFileContent(_fileType, _file);
if (!currentFileContent) {
return false;
}
auto currentContent = splitLines(*currentFileContent);
auto newContent = currentContent;
QStringList addedLines;
QStringList removedLines;
for (const auto &line : newContent) {
if (auto it = std::find(_previousContent.begin(),
_previousContent.end(),
line) == _previousContent.end()) {
addedLines.push_back(line);
}
}
for (const auto &line : _previousContent) {
if (std::find(newContent.begin(), newContent.end(), line) ==
newContent.end()) {
removedLines.push_back(line);
}
}
_previousContent = currentContent;
if (_changeMatchType == ChangeMatchType::ADDED ||
_changeMatchType == ChangeMatchType::ANY) {
for (const auto &line : addedLines) {
if (stringsMatch(line, _text, _regex)) {
return true;
}
}
}
if (_changeMatchType == ChangeMatchType::REMOVED ||
_changeMatchType == ChangeMatchType::ANY) {
for (const auto &line : removedLines) {
if (stringsMatch(line, _text, _regex)) {
return true;
}
}
}
return false;
}
bool MacroConditionFile::CheckCondition()
{
bool ret = false;
switch (_condition) {
case MacroConditionFile::ConditionType::MATCH:
if (_fileType == FileType::REMOTE) {
ret = CheckRemoteFileContent();
break;
}
ret = CheckLocalFileContent();
break;
return CheckFileContentMatch();
case MacroConditionFile::ConditionType::CONTENT_CHANGE:
ret = CheckChangeContent();
break;
return CheckChangeContent();
case MacroConditionFile::ConditionType::DATE_CHANGE:
ret = CheckChangeDate();
break;
return CheckChangeDate();
case MacroConditionFile::ConditionType::CHANGES_MATCH:
return CheckChangedMatch();
default:
break;
}
if (GetVariableValue().empty()) {
SetVariableValue(ret ? "true" : "false");
}
return ret;
return false;
}
bool MacroConditionFile::Save(obs_data_t *obj) const
@@ -173,9 +210,9 @@ bool MacroConditionFile::Save(obs_data_t *obj) const
_file.Save(obj, "file");
_text.Save(obj, "text");
obs_data_set_int(obj, "fileType", static_cast<int>(_fileType));
obs_data_set_int(obj, "changeMatchType",
static_cast<int>(_changeMatchType));
obs_data_set_int(obj, "condition", static_cast<int>(_condition));
obs_data_set_bool(obj, "useTime", _useTime);
obs_data_set_bool(obj, "onlyMatchIfChanged", _onlyMatchIfChanged);
return true;
}
@@ -191,10 +228,10 @@ bool MacroConditionFile::Load(obs_data_t *obj)
_file.Load(obj, "file");
_text.Load(obj, "text");
_fileType = static_cast<FileType>(obs_data_get_int(obj, "fileType"));
_changeMatchType = static_cast<ChangeMatchType>(
obs_data_get_int(obj, "changeMatchType"));
_condition =
static_cast<ConditionType>(obs_data_get_int(obj, "condition"));
_useTime = obs_data_get_bool(obj, "useTime");
_onlyMatchIfChanged = obs_data_get_bool(obj, "onlyMatchIfChanged");
return true;
}
@@ -205,39 +242,79 @@ std::string MacroConditionFile::GetShortDesc() const
static void populateFileTypes(QComboBox *list)
{
list->addItem(obs_module_text("AdvSceneSwitcher.condition.file.local"));
list->addItem(
obs_module_text("AdvSceneSwitcher.condition.file.remote"));
static constexpr std::array<
std::tuple<MacroConditionFile::FileType, std::string_view>, 2>
fileTypes = {{{MacroConditionFile::FileType::LOCAL,
"AdvSceneSwitcher.condition.file.local"},
{MacroConditionFile::FileType::REMOTE,
"AdvSceneSwitcher.condition.file.remote"}}};
for (const auto &[type, name] : fileTypes) {
list->addItem(obs_module_text(name.data()),
static_cast<int>(type));
}
}
static void populateConditions(QComboBox *list)
{
list->addItem(
obs_module_text("AdvSceneSwitcher.condition.file.type.match"));
list->addItem(obs_module_text(
"AdvSceneSwitcher.condition.file.type.contentChange"));
list->addItem(obs_module_text(
"AdvSceneSwitcher.condition.file.type.dateChange"));
static constexpr std::array<
std::tuple<MacroConditionFile::ConditionType, std::string_view>,
4>
conditionTypes = {{
{MacroConditionFile::ConditionType::MATCH,
"AdvSceneSwitcher.condition.file.type.match"},
{MacroConditionFile::ConditionType::CONTENT_CHANGE,
"AdvSceneSwitcher.condition.file.type.contentChange"},
{MacroConditionFile::ConditionType::DATE_CHANGE,
"AdvSceneSwitcher.condition.file.type.dateChange"},
{MacroConditionFile::ConditionType::CHANGES_MATCH,
"AdvSceneSwitcher.condition.file.type.changesMatch"},
}};
for (const auto &[type, name] : conditionTypes) {
list->addItem(obs_module_text(name.data()),
static_cast<int>(type));
}
}
static void populateChangeMatchTypes(QComboBox *list)
{
static constexpr std::array<
std::tuple<MacroConditionFile::ChangeMatchType, std::string_view>,
3>
matchTypes = {{
{MacroConditionFile::ChangeMatchType::ANY,
"AdvSceneSwitcher.condition.file.changeMatchType.any"},
{MacroConditionFile::ChangeMatchType::ADDED,
"AdvSceneSwitcher.condition.file.changeMatchType.added"},
{MacroConditionFile::ChangeMatchType::REMOVED,
"AdvSceneSwitcher.condition.file.changeMatchType.removed"},
}};
for (const auto &[type, name] : matchTypes) {
list->addItem(obs_module_text(name.data()),
static_cast<int>(type));
}
}
MacroConditionFileEdit::MacroConditionFileEdit(
QWidget *parent, std::shared_ptr<MacroConditionFile> entryData)
: QWidget(parent),
_fileTypes(new QComboBox()),
_changeMatchTypes(new QComboBox()),
_conditions(new QComboBox()),
_filePath(new FileSelection()),
_matchText(new VariableTextEdit(this)),
_regex(new RegexConfigWidget(parent)),
_checkModificationDate(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.fileTab.checkfileContentTime"))),
_checkFileContent(new QCheckBox(
obs_module_text("AdvSceneSwitcher.fileTab.checkfileContent")))
_regex(new RegexConfigWidget(parent))
{
populateFileTypes(_fileTypes);
populateConditions(_conditions);
populateChangeMatchTypes(_changeMatchTypes);
QWidget::connect(_fileTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(FileTypeChanged(int)));
QWidget::connect(_changeMatchTypes, SIGNAL(currentIndexChanged(int)),
this, SLOT(ChangeMatchTypeChanged(int)));
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ConditionChanged(int)));
QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this,
@@ -246,41 +323,30 @@ MacroConditionFileEdit::MacroConditionFileEdit(
SLOT(MatchTextChanged()));
QWidget::connect(_regex, SIGNAL(RegexConfigChanged(RegexConfig)), this,
SLOT(RegexChanged(RegexConfig)));
QWidget::connect(_checkModificationDate, SIGNAL(stateChanged(int)),
this, SLOT(CheckModificationDateChanged(int)));
QWidget::connect(_checkFileContent, SIGNAL(stateChanged(int)), this,
SLOT(OnlyMatchIfChangedChanged(int)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{fileType}}", _fileTypes},
{"{{changeMatchType}}", _changeMatchTypes},
{"{{conditions}}", _conditions},
{"{{filePath}}", _filePath},
{"{{matchText}}", _matchText},
{"{{useRegex}}", _regex},
{"{{checkModificationDate}}", _checkModificationDate},
{"{{checkFileContent}}", _checkFileContent},
};
QVBoxLayout *mainLayout = new QVBoxLayout;
QHBoxLayout *line1Layout = new QHBoxLayout;
QHBoxLayout *line2Layout = new QHBoxLayout;
QHBoxLayout *line3Layout = new QHBoxLayout;
auto line1Layout = new QHBoxLayout;
line1Layout->setContentsMargins(0, 0, 0, 0);
line2Layout->setContentsMargins(0, 0, 0, 0);
line3Layout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.file.entry.line1"),
line1Layout, widgetPlaceholders);
auto line2Layout = new QHBoxLayout;
line2Layout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.file.entry.line2"),
line2Layout, widgetPlaceholders, false);
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.file.entry.line3"),
line3Layout, widgetPlaceholders);
auto mainLayout = new QVBoxLayout;
mainLayout->addLayout(line1Layout);
mainLayout->addLayout(line2Layout);
mainLayout->addLayout(line3Layout);
setLayout(mainLayout);
_entryData = entryData;
@@ -294,21 +360,15 @@ void MacroConditionFileEdit::UpdateEntryData()
return;
}
_fileTypes->setCurrentIndex(static_cast<int>(_entryData->_fileType));
_conditions->setCurrentIndex(static_cast<int>(_entryData->_condition));
_fileTypes->setCurrentIndex(
_fileTypes->findData(static_cast<int>(_entryData->_fileType)));
_conditions->setCurrentIndex(_conditions->findData(
static_cast<int>(_entryData->_condition)));
_changeMatchTypes->setCurrentIndex(_changeMatchTypes->findData(
static_cast<int>(_entryData->_changeMatchType)));
_filePath->SetPath(_entryData->_file);
_matchText->setPlainText(_entryData->_text);
_regex->SetRegexConfig(_entryData->_regex);
_checkModificationDate->setChecked(_entryData->_useTime);
_checkFileContent->setChecked(_entryData->_onlyMatchIfChanged);
// TODO: Remove in future version
if (!_entryData->_useTime) {
_checkModificationDate->hide();
}
if (!_entryData->_onlyMatchIfChanged) {
_checkFileContent->hide();
}
SetWidgetVisibility();
}
@@ -319,19 +379,25 @@ void MacroConditionFileEdit::FileTypeChanged(int index)
return;
}
MacroConditionFile::FileType type =
static_cast<MacroConditionFile::FileType>(index);
auto type = static_cast<MacroConditionFile::FileType>(
_fileTypes->itemData(index).toInt());
_filePath->Button()->setEnabled(type ==
MacroConditionFile::FileType::LOCAL);
auto lock = LockContext();
_entryData->_fileType = type;
}
if (type == MacroConditionFile::FileType::LOCAL) {
_filePath->Button()->setDisabled(false);
_checkModificationDate->setDisabled(false);
} else {
_filePath->Button()->setDisabled(true);
_checkModificationDate->setDisabled(true);
void MacroConditionFileEdit::ChangeMatchTypeChanged(int index)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_fileType = type;
_entryData->_changeMatchType =
static_cast<MacroConditionFile::ChangeMatchType>(
_changeMatchTypes->itemData(index).toInt());
SetWidgetVisibility();
}
void MacroConditionFileEdit::ConditionChanged(int index)
@@ -341,8 +407,8 @@ void MacroConditionFileEdit::ConditionChanged(int index)
}
auto lock = LockContext();
_entryData->_condition =
static_cast<MacroConditionFile::ConditionType>(index);
_entryData->_condition = static_cast<MacroConditionFile::ConditionType>(
_conditions->itemData(index).toInt());
SetWidgetVisibility();
}
@@ -383,44 +449,25 @@ void MacroConditionFileEdit::RegexChanged(RegexConfig conf)
updateGeometry();
}
void MacroConditionFileEdit::CheckModificationDateChanged(int state)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_useTime = state;
}
void MacroConditionFileEdit::OnlyMatchIfChangedChanged(int state)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_onlyMatchIfChanged = state;
}
void MacroConditionFileEdit::SetWidgetVisibility()
{
if (!_entryData) {
return;
}
_matchText->setVisible(_entryData->_condition ==
MacroConditionFile::ConditionType::MATCH);
_regex->setVisible(_entryData->_condition ==
MacroConditionFile::ConditionType::MATCH);
_checkModificationDate->setVisible(
_entryData->_useTime &&
_changeMatchTypes->setVisible(
_entryData->_condition ==
MacroConditionFile::ConditionType::MATCH);
_checkFileContent->setVisible(
_entryData->_onlyMatchIfChanged &&
MacroConditionFile::ConditionType::CHANGES_MATCH);
_matchText->setVisible(
_entryData->_condition ==
MacroConditionFile::ConditionType::MATCH);
MacroConditionFile::ConditionType::MATCH ||
_entryData->_condition ==
MacroConditionFile::ConditionType::CHANGES_MATCH);
_regex->setVisible(
_entryData->_condition ==
MacroConditionFile::ConditionType::MATCH ||
_entryData->_condition ==
MacroConditionFile::ConditionType::CHANGES_MATCH);
adjustSize();
updateGeometry();
}

View File

@@ -10,6 +10,7 @@
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QStringList>
namespace advss {
@@ -35,27 +36,31 @@ public:
MATCH,
CONTENT_CHANGE,
DATE_CHANGE,
CHANGES_MATCH,
};
enum class ChangeMatchType {
ANY,
ADDED,
REMOVED,
};
StringVariable _file = obs_module_text("AdvSceneSwitcher.enterPath");
StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText");
FileType _fileType = FileType::LOCAL;
ChangeMatchType _changeMatchType = ChangeMatchType::ANY;
ConditionType _condition = ConditionType::MATCH;
RegexConfig _regex;
// TODO: Remove in future version
bool _useTime = false;
bool _onlyMatchIfChanged = false;
private:
bool MatchFileContent(QString &filedata);
bool CheckRemoteFileContent();
bool CheckLocalFileContent();
bool CheckFileContentMatch();
bool CheckChangeContent();
bool CheckChangeDate();
bool CheckChangedMatch();
QDateTime _lastMod;
size_t _lastHash = 0;
QStringList _previousContent;
static bool _registered;
static const std::string id;
};
@@ -78,23 +83,21 @@ public:
private slots:
void FileTypeChanged(int index);
void ChangeMatchTypeChanged(int index);
void ConditionChanged(int index);
void PathChanged(const QString &text);
void MatchTextChanged();
void RegexChanged(RegexConfig);
void CheckModificationDateChanged(int state);
void OnlyMatchIfChangedChanged(int state);
signals:
void HeaderInfoChanged(const QString &);
protected:
QComboBox *_fileTypes;
QComboBox *_changeMatchTypes;
QComboBox *_conditions;
FileSelection *_filePath;
VariableTextEdit *_matchText;
RegexConfigWidget *_regex;
QCheckBox *_checkModificationDate;
QCheckBox *_checkFileContent;
std::shared_ptr<MacroConditionFile> _entryData;
private:

View File

@@ -24,15 +24,14 @@ const static std::map<MacroConditionFilter::Condition, std::string>
"AdvSceneSwitcher.condition.filter.type.settingsChanged"},
};
bool MacroConditionFilter::CheckCondition()
bool MacroConditionFilter::CheckConditionHelper(const OBSWeakSource &filter)
{
auto filterWeakSource = _filter.GetFilter(_source);
if (!filterWeakSource) {
bool ret = false;
OBSSourceAutoRelease filterSource = obs_weak_source_get_source(filter);
if (!filterSource.Get()) {
return false;
}
auto filterSource = obs_weak_source_get_source(filterWeakSource);
bool ret = false;
switch (_condition) {
case Condition::ENABLED:
ret = obs_source_enabled(filterSource);
@@ -41,10 +40,9 @@ bool MacroConditionFilter::CheckCondition()
ret = !obs_source_enabled(filterSource);
break;
case Condition::SETTINGS_MATCH:
ret = CompareSourceSettings(filterWeakSource, _settings,
_regex);
ret = CompareSourceSettings(filter, _settings, _regex);
if (IsReferencedInVars()) {
SetVariableValue(GetSourceSettings(filterWeakSource));
SetVariableValue(GetSourceSettings(filter));
}
break;
case Condition::SETTINGS_CHANGED: {
@@ -57,8 +55,20 @@ bool MacroConditionFilter::CheckCondition()
default:
break;
}
return ret;
}
obs_source_release(filterSource);
bool MacroConditionFilter::CheckCondition()
{
auto filters = _filter.GetFilters(_source);
if (filters.empty()) {
return false;
}
bool ret = true;
for (const auto &filter : filters) {
ret = ret && CheckConditionHelper(filter);
}
if (GetVariableValue().empty()) {
SetVariableValue(ret ? "true" : "false");
@@ -211,12 +221,12 @@ void MacroConditionFilterEdit::ConditionChanged(int index)
void MacroConditionFilterEdit::GetSettingsClicked()
{
if (_loading || !_entryData ||
!_entryData->_filter.GetFilter(_entryData->_source)) {
_entryData->_filter.GetFilters(_entryData->_source).empty()) {
return;
}
QString json = FormatJsonString(GetSourceSettings(
_entryData->_filter.GetFilter(_entryData->_source)));
_entryData->_filter.GetFilters(_entryData->_source).at(0)));
if (_entryData->_regex.Enabled()) {
json = EscapeForRegex(json);
}

View File

@@ -38,8 +38,9 @@ public:
RegexConfig _regex;
private:
std::string _currentSettings;
bool CheckConditionHelper(const OBSWeakSource &);
std::string _currentSettings;
static bool _registered;
static const std::string id;
};

View File

@@ -24,6 +24,8 @@ const static std::map<MacroConditionMacro::Type, std::string>
"AdvSceneSwitcher.condition.macro.type.actionDisabled"},
{MacroConditionMacro::Type::ACTION_ENABLED,
"AdvSceneSwitcher.condition.macro.type.actionEnabled"},
{MacroConditionMacro::Type::PAUSED,
"AdvSceneSwitcher.condition.macro.type.paused"},
};
const static std::map<MacroConditionMacro::CounterCondition, std::string>
@@ -75,6 +77,8 @@ bool MacroConditionMacro::CheckMultiStateCondition()
}
}
SetTempVarValue("matchedCount", std::to_string(matchedCount));
switch (_multiSateCondition) {
case MacroConditionMacro::MultiStateCondition::BELOW:
return matchedCount < _multiSateCount;
@@ -107,6 +111,16 @@ bool MacroConditionMacro::CheckActionStateCondition()
return false;
}
bool MacroConditionMacro::CheckPauseState()
{
auto macro = _macro.GetMacro();
if (!macro) {
return false;
}
return macro->Paused();
}
bool MacroConditionMacro::CheckCountCondition()
{
auto macro = _macro.GetMacro();
@@ -114,6 +128,8 @@ bool MacroConditionMacro::CheckCountCondition()
return false;
}
SetTempVarValue("runCount", std::to_string(macro->RunCount()));
switch (_counterCondition) {
case CounterCondition::BELOW:
return macro->RunCount() < _count;
@@ -140,6 +156,8 @@ bool MacroConditionMacro::CheckCondition()
case Type::ACTION_DISABLED:
case Type::ACTION_ENABLED:
return CheckActionStateCondition();
case Type::PAUSED:
return CheckPauseState();
default:
break;
}
@@ -193,7 +211,7 @@ bool MacroConditionMacro::Load(obs_data_t *obj)
bool MacroConditionMacro::PostLoad()
{
return MacroRefCondition::PostLoad() &&
return MacroCondition::PostLoad() && MacroRefCondition::PostLoad() &&
MultiMacroRefCondtition::PostLoad();
}
@@ -202,6 +220,45 @@ std::string MacroConditionMacro::GetShortDesc() const
return _macro.Name();
}
void MacroConditionMacro::SetType(Type type)
{
_type = type;
SetupTempVars();
}
void MacroConditionMacro::SetupTempVars()
{
MacroCondition::SetupTempVars();
switch (_type) {
case Type::COUNT:
AddTempvar(
"runCount",
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.runCount"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.runCount.description"));
break;
case Type::STATE:
break;
case Type::MULTI_STATE:
AddTempvar(
"matchedCount",
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.matchedCount"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.matchedCount.description"));
break;
case Type::ACTION_DISABLED:
break;
case Type::ACTION_ENABLED:
break;
case Type::PAUSED:
break;
default:
break;
}
}
static inline void populateTypeSelection(QComboBox *list)
{
for (auto entry : macroConditionTypes) {
@@ -335,7 +392,10 @@ void MacroConditionMacroEdit::ClearLayouts()
void MacroConditionMacroEdit::SetupWidgets()
{
switch (_entryData->_type) {
SetWidgetVisibility();
ClearLayouts();
switch (_entryData->GetType()) {
case MacroConditionMacro::Type::COUNT:
SetupCountWidgets();
break;
@@ -351,6 +411,9 @@ void MacroConditionMacroEdit::SetupWidgets()
case MacroConditionMacro::Type::ACTION_ENABLED:
SetupActionStateWidgets(true);
break;
case MacroConditionMacro::Type::PAUSED:
SetupPauseWidgets();
break;
default:
break;
}
@@ -358,38 +421,24 @@ void MacroConditionMacroEdit::SetupWidgets()
void MacroConditionMacroEdit::SetupStateWidgets()
{
SetWidgetVisibility();
ClearLayouts();
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{macros}}", _macros},
};
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.macro.state.entry"),
_settingsLine1, widgetPlaceholders);
_settingsLine1, {{"{{macros}}", _macros}});
}
void MacroConditionMacroEdit::SetupMultiStateWidgets()
{
SetWidgetVisibility();
ClearLayouts();
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{multiStateConditions}}", _multiStateConditions},
{"{{multiStateCount}}", _multiStateCount},
};
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.macro.multistate.entry"),
_settingsLine1, widgetPlaceholders);
_settingsLine1,
{{"{{multiStateConditions}}", _multiStateConditions},
{"{{multiStateCount}}", _multiStateCount}});
}
void MacroConditionMacroEdit::SetupCountWidgets()
{
SetWidgetVisibility();
ClearLayouts();
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{macros}}", _macros},
{"{{conditions}}", _counterConditions},
{"{{count}}", _count},
@@ -408,9 +457,6 @@ void MacroConditionMacroEdit::SetupCountWidgets()
void MacroConditionMacroEdit::SetupActionStateWidgets(bool enable)
{
SetWidgetVisibility();
ClearLayouts();
PlaceWidgets(
obs_module_text(
enable ? "AdvSceneSwitcher.condition.macro.actionState.enabled.entry"
@@ -419,35 +465,49 @@ void MacroConditionMacroEdit::SetupActionStateWidgets(bool enable)
{{"{{macros}}", _macros}, {"{{actionIndex}}", _actionIndex}});
}
void MacroConditionMacroEdit::SetupPauseWidgets()
{
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.condition.macro.paused.entry"),
_settingsLine1, {{"{{macros}}", _macros}});
}
void MacroConditionMacroEdit::SetWidgetVisibility()
{
_macros->setVisible(
_entryData->_type == MacroConditionMacro::Type::COUNT ||
_entryData->_type == MacroConditionMacro::Type::STATE ||
_entryData->_type ==
_entryData->GetType() == MacroConditionMacro::Type::COUNT ||
_entryData->GetType() == MacroConditionMacro::Type::STATE ||
_entryData->GetType() ==
MacroConditionMacro::Type::ACTION_DISABLED ||
_entryData->_type == MacroConditionMacro::Type::ACTION_ENABLED);
_counterConditions->setVisible(_entryData->_type ==
_entryData->GetType() ==
MacroConditionMacro::Type::ACTION_ENABLED ||
_entryData->GetType() == MacroConditionMacro::Type::PAUSED);
_counterConditions->setVisible(_entryData->GetType() ==
MacroConditionMacro::Type::COUNT);
_count->setVisible(_entryData->_type ==
_count->setVisible(_entryData->GetType() ==
MacroConditionMacro::Type::COUNT);
_currentCount->setVisible(_entryData->_type ==
_currentCount->setVisible(_entryData->GetType() ==
MacroConditionMacro::Type::COUNT);
_resetCount->setVisible(_entryData->_type ==
_resetCount->setVisible(_entryData->GetType() ==
MacroConditionMacro::Type::COUNT);
_macroList->setVisible(_entryData->_type ==
_macroList->setVisible(_entryData->GetType() ==
MacroConditionMacro::Type::MULTI_STATE);
_multiStateConditions->setVisible(
_entryData->_type == MacroConditionMacro::Type::MULTI_STATE);
_multiStateCount->setVisible(_entryData->_type ==
_entryData->GetType() ==
MacroConditionMacro::Type::MULTI_STATE);
_multiStateCount->setVisible(_entryData->GetType() ==
MacroConditionMacro::Type::MULTI_STATE);
_actionIndex->setVisible(
_entryData->_type ==
_entryData->GetType() ==
MacroConditionMacro::Type::ACTION_DISABLED ||
_entryData->_type == MacroConditionMacro::Type::ACTION_ENABLED);
if (_entryData->_type == MacroConditionMacro::Type::MULTI_STATE ||
_entryData->_type == MacroConditionMacro::Type::ACTION_DISABLED ||
_entryData->_type == MacroConditionMacro::Type::ACTION_ENABLED) {
_entryData->GetType() ==
MacroConditionMacro::Type::ACTION_ENABLED);
if (_entryData->GetType() == MacroConditionMacro::Type::MULTI_STATE ||
_entryData->GetType() ==
MacroConditionMacro::Type::ACTION_DISABLED ||
_entryData->GetType() ==
MacroConditionMacro::Type::ACTION_ENABLED ||
_entryData->GetType() == MacroConditionMacro::Type::PAUSED) {
_pausedWarning->hide();
}
@@ -463,7 +523,7 @@ void MacroConditionMacroEdit::UpdateEntryData()
SetupWidgets();
_macros->SetCurrentMacro(_entryData->_macro);
_types->setCurrentIndex(static_cast<int>(_entryData->_type));
_types->setCurrentIndex(static_cast<int>(_entryData->GetType()));
_counterConditions->setCurrentIndex(
static_cast<int>(_entryData->_counterCondition));
_count->SetValue(_entryData->_count);
@@ -535,7 +595,7 @@ void MacroConditionMacroEdit::TypeChanged(int type)
}
auto lock = LockContext();
_entryData->_type = static_cast<MacroConditionMacro::Type>(type);
_entryData->SetType(static_cast<MacroConditionMacro::Type>(type));
SetupWidgets();
}
@@ -571,7 +631,8 @@ void MacroConditionMacroEdit::UpdatePaused()
auto macro = _entryData->_macro.GetMacro();
_pausedWarning->setVisible(
_entryData &&
_entryData->_type != MacroConditionMacro::Type::MULTI_STATE &&
_entryData->GetType() !=
MacroConditionMacro::Type::MULTI_STATE &&
macro && macro->Paused());
adjustSize();
updateGeometry();

View File

@@ -39,8 +39,10 @@ public:
MULTI_STATE,
ACTION_DISABLED,
ACTION_ENABLED,
PAUSED,
};
Type _type = Type::STATE;
void SetType(Type);
Type GetType() const { return _type; }
enum class CounterCondition {
BELOW,
@@ -64,7 +66,11 @@ private:
bool CheckStateCondition();
bool CheckMultiStateCondition();
bool CheckActionStateCondition();
bool CheckPauseState();
void SetupTempVars();
Type _type = Type::STATE;
static bool _registered;
static const std::string id;
};
@@ -128,6 +134,7 @@ private:
void SetupMultiStateWidgets();
void SetupCountWidgets();
void SetupActionStateWidgets(bool enable);
void SetupPauseWidgets();
void SetWidgetVisibility();
bool _loading = true;
};

View File

@@ -19,25 +19,43 @@ bool MacroConditionProcess::CheckCondition()
QStringList runningProcesses;
QString proc = QString::fromStdString(_process);
GetProcessList(runningProcesses);
std::string foregroundProcessName;
GetForegroundProcessName(foregroundProcessName);
bool equals = runningProcesses.contains(proc);
bool matches = runningProcesses.indexOf(QRegularExpression(proc)) != -1;
bool focus = !_focus || IsInFocus(proc);
SetVariableValue(foregroundProcessName);
if (IsReferencedInVars()) {
std::string name;
GetForegroundProcessName(name);
SetVariableValue(name);
if (!_regex.Enabled()) {
if (runningProcesses.contains(proc) &&
(!_checkFocus || IsInFocus(proc))) {
SetTempVarValue("name", proc.toStdString());
return true;
}
return false;
}
return (equals || matches) && focus;
auto matchIndex = runningProcesses.indexOf(QRegularExpression(proc));
if (matchIndex == -1) {
return false;
}
if (!_checkFocus) {
SetTempVarValue("name",
runningProcesses.at(matchIndex).toStdString());
return true;
}
if (!IsInFocus(proc)) {
return false;
}
SetTempVarValue("name", foregroundProcessName);
return true;
}
bool MacroConditionProcess::Save(obs_data_t *obj) const
{
MacroCondition::Save(obj);
obs_data_set_string(obj, "process", _process.c_str());
obs_data_set_bool(obj, "focus", _focus);
obs_data_set_bool(obj, "focus", _checkFocus);
_regex.Save(obj);
obs_data_set_int(obj, "version", 1);
return true;
}
@@ -45,7 +63,13 @@ bool MacroConditionProcess::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_process = obs_data_get_string(obj, "process");
_focus = obs_data_get_bool(obj, "focus");
_checkFocus = obs_data_get_bool(obj, "focus");
// Fall back to partial match regex as default for old version
if (!obs_data_has_user_value(obj, "version")) {
_regex.SetEnabled(true);
} else {
_regex.Load(obj);
}
return true;
}
@@ -54,10 +78,18 @@ std::string MacroConditionProcess::GetShortDesc() const
return _process;
}
void MacroConditionProcess::SetupTempVars()
{
MacroCondition::SetupTempVars();
AddTempvar("name",
obs_module_text("AdvSceneSwitcher.tempVar.process.name"));
}
MacroConditionProcessEdit::MacroConditionProcessEdit(
QWidget *parent, std::shared_ptr<MacroConditionProcess> entryData)
: QWidget(parent),
_processSelection(new QComboBox()),
_regex(new RegexConfigWidget(this)),
_focused(new QCheckBox()),
_focusProcess(new QLabel()),
_focusLayout(new QHBoxLayout())
@@ -68,6 +100,8 @@ MacroConditionProcessEdit::MacroConditionProcessEdit(
QWidget::connect(_processSelection,
SIGNAL(currentTextChanged(const QString &)), this,
SLOT(ProcessChanged(const QString &)));
QWidget::connect(_regex, SIGNAL(RegexConfigChanged(RegexConfig)), this,
SLOT(RegexChanged(RegexConfig)));
QWidget::connect(_focused, SIGNAL(stateChanged(int)), this,
SLOT(FocusChanged(int)));
QWidget::connect(&_timer, SIGNAL(timeout()), this,
@@ -77,6 +111,7 @@ MacroConditionProcessEdit::MacroConditionProcessEdit(
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{processes}}", _processSelection},
{"{{regex}}", _regex},
{"{{focused}}", _focused},
{"{{focusProcess}}", _focusProcess},
};
@@ -112,6 +147,18 @@ void MacroConditionProcessEdit::ProcessChanged(const QString &text)
QString::fromStdString(_entryData->GetShortDesc()));
}
void MacroConditionProcessEdit::RegexChanged(RegexConfig conf)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_regex = conf;
adjustSize();
updateGeometry();
}
void MacroConditionProcessEdit::FocusChanged(int state)
{
if (_loading || !_entryData) {
@@ -119,7 +166,7 @@ void MacroConditionProcessEdit::FocusChanged(int state)
}
auto lock = LockContext();
_entryData->_focus = state;
_entryData->_checkFocus = state;
SetWidgetVisibility();
}
@@ -134,7 +181,7 @@ void MacroConditionProcessEdit::SetWidgetVisibility()
if (!_entryData) {
return;
}
SetLayoutVisible(_focusLayout, _entryData->_focus);
SetLayoutVisible(_focusLayout, _entryData->_checkFocus);
adjustSize();
}
@@ -145,7 +192,8 @@ void MacroConditionProcessEdit::UpdateEntryData()
}
_processSelection->setCurrentText(_entryData->_process.c_str());
_focused->setChecked(_entryData->_focus);
_regex->SetRegexConfig(_entryData->_regex);
_focused->setChecked(_entryData->_checkFocus);
SetWidgetVisibility();
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include "macro-condition-edit.hpp"
#include "regex-config.hpp"
#include <QComboBox>
#include <QCheckBox>
@@ -20,9 +21,12 @@ public:
}
std::string _process;
bool _focus = true;
bool _checkFocus = true;
RegexConfig _regex = RegexConfig::PartialMatchRegexConfig();
private:
void SetupTempVars();
static bool _registered;
static const std::string id;
};
@@ -45,22 +49,22 @@ public:
private slots:
void ProcessChanged(const QString &text);
void RegexChanged(RegexConfig);
void FocusChanged(int state);
void UpdateFocusProcess();
signals:
void HeaderInfoChanged(const QString &);
protected:
private:
void SetWidgetVisibility();
QComboBox *_processSelection;
RegexConfigWidget *_regex;
QCheckBox *_focused;
QLabel *_focusProcess;
QHBoxLayout *_focusLayout;
QTimer _timer;
std::shared_ptr<MacroConditionProcess> _entryData;
private:
void SetWidgetVisibility();
bool _loading = true;
};

View File

@@ -1,6 +1,8 @@
#include "macro-condition-recording.hpp"
#include "utility.hpp"
#include <thread>
namespace advss {
const std::string MacroConditionRecord::id = "recording";
@@ -10,45 +12,81 @@ bool MacroConditionRecord::_registered = MacroConditionFactory::Register(
{MacroConditionRecord::Create, MacroConditionRecordEdit::Create,
"AdvSceneSwitcher.condition.record"});
const static std::map<RecordState, std::string> recordStates = {
{RecordState::STOP, "AdvSceneSwitcher.condition.record.state.stop"},
{RecordState::PAUSE, "AdvSceneSwitcher.condition.record.state.pause"},
{RecordState::START, "AdvSceneSwitcher.condition.record.state.start"},
const static std::map<MacroConditionRecord::Condition, std::string>
recordStates = {
{MacroConditionRecord::Condition::STOP,
"AdvSceneSwitcher.condition.record.state.stop"},
{MacroConditionRecord::Condition::PAUSE,
"AdvSceneSwitcher.condition.record.state.pause"},
{MacroConditionRecord::Condition::START,
"AdvSceneSwitcher.condition.record.state.start"},
{MacroConditionRecord::Condition::DURATION,
"AdvSceneSwitcher.condition.record.state.duration"},
};
static bool SetupRecordingTimer();
static bool recordingTimerIsSetup = SetupRecordingTimer();
static int currentRecordingDurationInSeconds = 0;
bool MacroConditionRecord::CheckCondition()
{
bool stateMatch = false;
switch (_recordState) {
case RecordState::STOP:
stateMatch = !obs_frontend_recording_active();
break;
case RecordState::PAUSE:
stateMatch = obs_frontend_recording_paused();
break;
case RecordState::START:
stateMatch = obs_frontend_recording_active();
break;
switch (_condition) {
case Condition::STOP:
return !obs_frontend_recording_active();
case Condition::PAUSE:
return obs_frontend_recording_paused();
case Condition::START:
return obs_frontend_recording_active();
case Condition::DURATION:
SetTempVarValue(
"durationSeconds",
std::to_string(currentRecordingDurationInSeconds));
return currentRecordingDurationInSeconds > _duration.Seconds();
default:
break;
}
return stateMatch;
return false;
}
bool MacroConditionRecord::Save(obs_data_t *obj) const
{
MacroCondition::Save(obj);
obs_data_set_int(obj, "state", static_cast<int>(_recordState));
obs_data_set_int(obj, "state", static_cast<int>(_condition));
_duration.Save(obj);
return true;
}
bool MacroConditionRecord::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_recordState = static_cast<RecordState>(obs_data_get_int(obj, "state"));
_condition = static_cast<Condition>(obs_data_get_int(obj, "state"));
_duration.Load(obj);
return true;
}
void MacroConditionRecord::SetCondition(Condition condition)
{
_condition = condition;
SetupTempVars();
}
void MacroConditionRecord::SetupTempVars()
{
MacroCondition::SetupTempVars();
switch (_condition) {
case Condition::DURATION:
AddTempvar(
"durationSeconds",
obs_module_text(
"AdvSceneSwitcher.tempVar.recording.durationSeconds"),
obs_module_text(
"AdvSceneSwitcher.tempVar.recording.durationSeconds.description"));
break;
default:
break;
}
}
static inline void populateStateSelection(QComboBox *list)
{
for (auto entry : recordStates) {
@@ -58,38 +96,39 @@ static inline void populateStateSelection(QComboBox *list)
MacroConditionRecordEdit::MacroConditionRecordEdit(
QWidget *parent, std::shared_ptr<MacroConditionRecord> entryData)
: QWidget(parent)
: QWidget(parent),
_condition(new QComboBox(this)),
_duration(new DurationSelection(this))
{
_recordState = new QComboBox();
populateStateSelection(_condition);
QWidget::connect(_recordState, SIGNAL(currentIndexChanged(int)), this,
SLOT(StateChanged(int)));
populateStateSelection(_recordState);
QHBoxLayout *mainLayout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{recordState}}", _recordState},
};
QWidget::connect(_condition, SIGNAL(currentIndexChanged(int)), this,
SLOT(ConditionChanged(int)));
QWidget::connect(_duration, SIGNAL(DurationChanged(const Duration &)),
this, SLOT(DurationChanged(const Duration &)));
auto layout = new QHBoxLayout;
PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.record.entry"),
mainLayout, widgetPlaceholders);
setLayout(mainLayout);
layout,
{{"{{condition}}", _condition},
{"{{duration}}", _duration}});
setLayout(layout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroConditionRecordEdit::StateChanged(int value)
void MacroConditionRecordEdit::ConditionChanged(int value)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_recordState = static_cast<RecordState>(value);
_entryData->SetCondition(
static_cast<MacroConditionRecord::Condition>(value));
SetWidgetVisibility();
}
void MacroConditionRecordEdit::UpdateEntryData()
@@ -98,8 +137,65 @@ void MacroConditionRecordEdit::UpdateEntryData()
return;
}
_recordState->setCurrentIndex(
static_cast<int>(_entryData->_recordState));
_condition->setCurrentIndex(
static_cast<int>(_entryData->GetCondition()));
_duration->SetDuration(_entryData->_duration);
SetWidgetVisibility();
}
void MacroConditionRecordEdit::DurationChanged(const Duration &duration)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_duration = duration;
}
void MacroConditionRecordEdit::SetWidgetVisibility()
{
_duration->setVisible(_entryData->GetCondition() ==
MacroConditionRecord::Condition::DURATION);
}
bool SetupRecordingTimer()
{
static std::atomic_bool recordingStopped = {true};
static std::atomic_bool recordingPaused = {false};
static auto handleRecordingEvents = [](enum obs_frontend_event event,
void *) {
switch (event) {
case OBS_FRONTEND_EVENT_RECORDING_STOPPED:
recordingStopped = true;
break;
case OBS_FRONTEND_EVENT_RECORDING_STARTED:
recordingStopped = false;
break;
case OBS_FRONTEND_EVENT_RECORDING_PAUSED:
recordingPaused = true;
break;
case OBS_FRONTEND_EVENT_RECORDING_UNPAUSED:
recordingPaused = false;
break;
};
};
obs_frontend_add_event_callback(handleRecordingEvents, nullptr);
std::thread thread([]() {
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (recordingStopped) {
currentRecordingDurationInSeconds = 0;
continue;
}
if (recordingPaused) {
continue;
}
++currentRecordingDurationInSeconds;
}
});
thread.detach();
return true;
}
} // namespace advss

View File

@@ -6,12 +6,6 @@
namespace advss {
enum class RecordState {
STOP,
PAUSE,
START,
};
class MacroConditionRecord : public MacroCondition {
public:
MacroConditionRecord(Macro *m) : MacroCondition(m) {}
@@ -24,9 +18,21 @@ public:
return std::make_shared<MacroConditionRecord>(m);
}
RecordState _recordState = RecordState::STOP;
enum class Condition {
STOP,
PAUSE,
START,
DURATION,
};
void SetCondition(Condition);
Condition GetCondition() const { return _condition; }
Duration _duration;
private:
void SetupTempVars();
Condition _condition = Condition::STOP;
static bool _registered;
static const std::string id;
};
@@ -48,13 +54,15 @@ public:
}
private slots:
void StateChanged(int value);
protected:
QComboBox *_recordState;
std::shared_ptr<MacroConditionRecord> _entryData;
void ConditionChanged(int value);
void DurationChanged(const Duration &);
private:
void SetWidgetVisibility();
QComboBox *_condition;
DurationSelection *_duration;
std::shared_ptr<MacroConditionRecord> _entryData;
bool _loading = true;
};

View File

@@ -12,19 +12,79 @@ bool MacroConditionSceneTransform::_registered =
MacroConditionSceneTransformEdit::Create,
"AdvSceneSwitcher.condition.sceneTransform"});
bool MacroConditionSceneTransform::CheckCondition()
const static std::map<MacroConditionSceneTransform::CondType, std::string>
conditionTypes = {
{MacroConditionSceneTransform::CondType::MATCHES,
"AdvSceneSwitcher.condition.sceneTransform.entry.type.matches"},
{MacroConditionSceneTransform::CondType::CHANGED,
"AdvSceneSwitcher.condition.sceneTransform.entry.type.changed"},
};
static bool doesTransformOfAnySceneItemMatch(
const std::vector<OBSSceneItem> &items, const std::string &jsonCompare,
const RegexConfig &regex, std::string &newVariable)
{
bool ret = false;
auto items = _source.GetSceneItems(_scene);
std::string json;
for (const auto &item : items) {
json = GetSceneItemTransform(item);
if (MatchJson(json, _settings, _regex)) {
if (MatchJson(json, jsonCompare, regex)) {
ret = true;
}
}
SetVariableValue(json);
newVariable = json;
return ret;
}
static bool
didTransformOfAnySceneItemChange(const std::vector<OBSSceneItem> &items,
std::vector<std::string> &previousTransform,
std::string &newVariable)
{
bool ret = false;
std::string json;
RegexConfig regex;
auto numItems = items.size();
if (previousTransform.size() < numItems) {
ret = true;
previousTransform.resize(numItems);
}
for (size_t idx = 0; idx < numItems; ++idx) {
auto const &item = items[idx];
json = GetSceneItemTransform(item);
if (!MatchJson(json, previousTransform[idx], regex)) {
ret = true;
previousTransform[idx] = json;
}
}
newVariable = json;
return ret;
}
bool MacroConditionSceneTransform::CheckCondition()
{
auto items = _source.GetSceneItems(_scene);
if (items.empty()) {
return false;
}
std::string newVariable = "";
bool ret = false;
switch (_type) {
case CondType::MATCHES:
ret = doesTransformOfAnySceneItemMatch(items, _settings, _regex,
newVariable);
break;
case CondType::CHANGED:
ret = didTransformOfAnySceneItemChange(
items, _previousTransform, newVariable);
break;
default:
return false;
}
SetVariableValue(newVariable);
return ret;
}
@@ -35,6 +95,7 @@ bool MacroConditionSceneTransform::Save(obs_data_t *obj) const
_source.Save(obj);
_settings.Save(obj, "settings");
_regex.Save(obj);
obs_data_set_int(obj, "type", static_cast<int>(_type));
return true;
}
@@ -52,6 +113,7 @@ bool MacroConditionSceneTransform::Load(obs_data_t *obj)
_source.Load(obj);
_settings.Load(obj, "settings");
_regex.Load(obj);
_type = static_cast<CondType>(obs_data_get_int(obj, "type"));
// TOOD: remove in future version
if (obs_data_has_user_value(obj, "regex")) {
_regex.CreateBackwardsCompatibleRegex(
@@ -69,6 +131,13 @@ std::string MacroConditionSceneTransform::GetShortDesc() const
return _scene.ToString() + " - " + _source.ToString();
}
static inline void populateTypesSelection(QComboBox *list)
{
for (const auto &entry : conditionTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
}
}
MacroConditionSceneTransformEdit::MacroConditionSceneTransformEdit(
QWidget *parent,
std::shared_ptr<MacroConditionSceneTransform> entryData)
@@ -76,11 +145,14 @@ MacroConditionSceneTransformEdit::MacroConditionSceneTransformEdit(
_scenes(new SceneSelectionWidget(window(), true, false, false, true)),
_sources(new SceneItemSelectionWidget(
parent, true, SceneItemSelectionWidget::Placeholder::ANY)),
_types(new QComboBox()),
_getSettings(new QPushButton(obs_module_text(
"AdvSceneSwitcher.condition.sceneTransform.getTransform"))),
_settings(new VariableTextEdit(this)),
_regex(new RegexConfigWidget(parent))
{
populateTypesSelection(_types);
QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
this, SLOT(SceneChanged(const SceneSelection &)));
QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
@@ -88,6 +160,8 @@ MacroConditionSceneTransformEdit::MacroConditionSceneTransformEdit(
QWidget::connect(_sources,
SIGNAL(SceneItemChanged(const SceneItemSelection &)),
this, SLOT(SourceChanged(const SceneItemSelection &)));
QWidget::connect(_types, SIGNAL(currentIndexChanged(int)), this,
SLOT(TypeChanged(int)));
QWidget::connect(_getSettings, SIGNAL(clicked()), this,
SLOT(GetSettingsClicked()));
QWidget::connect(_settings, SIGNAL(textChanged()), this,
@@ -96,8 +170,11 @@ MacroConditionSceneTransformEdit::MacroConditionSceneTransformEdit(
SLOT(RegexChanged(RegexConfig)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{scenes}}", _scenes}, {"{{sources}}", _sources},
{"{{settings}}", _settings}, {"{{getSettings}}", _getSettings},
{"{{scenes}}", _scenes},
{"{{sources}}", _sources},
{"{{types}}", _types},
{"{{settings}}", _settings},
{"{{getSettings}}", _getSettings},
{"{{regex}}", _regex},
};
@@ -136,11 +213,10 @@ void MacroConditionSceneTransformEdit::UpdateEntryData()
_scenes->SetScene(_entryData->_scene);
_sources->SetSceneItem(_entryData->_source);
_types->setCurrentIndex(static_cast<int>(_entryData->_type));
_regex->SetRegexConfig(_entryData->_regex);
_settings->setPlainText(_entryData->_settings);
adjustSize();
updateGeometry();
SetWidgetVisibility();
}
void MacroConditionSceneTransformEdit::SceneChanged(const SceneSelection &s)
@@ -168,6 +244,18 @@ void MacroConditionSceneTransformEdit::SourceChanged(
updateGeometry();
}
void MacroConditionSceneTransformEdit::TypeChanged(int index)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_type =
static_cast<MacroConditionSceneTransform::CondType>(index);
SetWidgetVisibility();
}
void MacroConditionSceneTransformEdit::GetSettingsClicked()
{
@@ -213,4 +301,16 @@ void MacroConditionSceneTransformEdit::RegexChanged(RegexConfig conf)
updateGeometry();
}
void MacroConditionSceneTransformEdit::SetWidgetVisibility()
{
const bool showSettingsAndRegex =
_entryData->_type ==
MacroConditionSceneTransform::CondType::MATCHES;
_settings->setVisible(showSettingsAndRegex);
_getSettings->setVisible(showSettingsAndRegex);
_regex->setVisible(showSettingsAndRegex);
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@@ -23,12 +23,20 @@ public:
return std::make_shared<MacroConditionSceneTransform>(m);
}
enum class CondType {
MATCHES,
CHANGED,
};
SceneSelection _scene;
SceneItemSelection _source;
CondType _type = CondType::MATCHES;
RegexConfig _regex;
StringVariable _settings = "";
private:
std::vector<std::string> _previousTransform;
static bool _registered;
static const std::string id;
};
@@ -54,6 +62,7 @@ public:
private slots:
void SceneChanged(const SceneSelection &);
void SourceChanged(const SceneItemSelection &);
void TypeChanged(int type);
void GetSettingsClicked();
void SettingsChanged();
void RegexChanged(RegexConfig);
@@ -63,6 +72,7 @@ signals:
protected:
SceneSelectionWidget *_scenes;
SceneItemSelectionWidget *_sources;
QComboBox *_types;
QPushButton *_getSettings;
VariableTextEdit *_settings;
RegexConfigWidget *_regex;
@@ -70,6 +80,7 @@ protected:
std::shared_ptr<MacroConditionSceneTransform> _entryData;
private:
void SetWidgetVisibility();
bool _loading = true;
};

View File

@@ -76,11 +76,13 @@ bool MacroConditionScene::CheckCondition()
case Type::CURRENT: {
auto scene = getCurrentSceneHelper(_useTransitionTargetScene);
SetVariableValue(GetWeakSourceName(scene));
SetTempVarValue("current", GetWeakSourceName(scene));
return scene == _scene.GetScene(false);
}
case Type::PREVIOUS: {
auto scene = getPreviousSceneHelper(_useTransitionTargetScene);
SetVariableValue(GetWeakSourceName(scene));
SetTempVarValue("previous", GetWeakSourceName(scene));
return scene == _scene.GetScene(false);
}
case Type::PREVIEW: {
@@ -89,22 +91,29 @@ bool MacroConditionScene::CheckCondition()
OBSWeakSourceAutoRelease scene =
obs_source_get_weak_source(source);
SetVariableValue(GetWeakSourceName(scene));
SetTempVarValue("preview", GetWeakSourceName(scene));
return scene == _scene.GetScene(false);
}
case Type::CHANGED:
SetVariableValue(GetWeakSourceName(switcher->currentScene));
SetTempVarValue("current",
GetWeakSourceName(switcher->currentScene));
return sceneChanged;
case Type::NOT_CHANGED:
SetVariableValue(GetWeakSourceName(switcher->currentScene));
SetTempVarValue("current",
GetWeakSourceName(switcher->currentScene));
return !sceneChanged;
case Type::CURRENT_PATTERN: {
auto scene = getCurrentSceneHelper(_useTransitionTargetScene);
SetVariableValue(GetWeakSourceName(scene));
SetTempVarValue("current", GetWeakSourceName(scene));
return sceneNameMatchesRegex(scene, _pattern);
}
case Type::PREVIOUS_PATTERN: {
auto scene = getPreviousSceneHelper(_useTransitionTargetScene);
SetVariableValue(GetWeakSourceName(scene));
SetTempVarValue("previous", GetWeakSourceName(scene));
return sceneNameMatchesRegex(scene, _pattern);
}
case Type::PREVIEW_PATTERN: {
@@ -113,6 +122,7 @@ bool MacroConditionScene::CheckCondition()
OBSWeakSourceAutoRelease scene =
obs_source_get_weak_source(source);
SetVariableValue(GetWeakSourceName(scene));
SetTempVarValue("preview", GetWeakSourceName(scene));
return sceneNameMatchesRegex(scene.Get(), _pattern);
}
}
@@ -196,6 +206,41 @@ std::string MacroConditionScene::GetShortDesc() const
return "";
}
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(
"AdvSceneSwitcher.tempVar.scene.current"));
break;
case Type::PREVIOUS:
case Type::PREVIOUS_PATTERN:
AddTempvar("previous",
obs_module_text(
"AdvSceneSwitcher.tempVar.scene.previous"));
break;
case Type::PREVIEW:
case Type::PREVIEW_PATTERN:
AddTempvar("preview",
obs_module_text(
"AdvSceneSwitcher.tempVar.scene.preview"));
break;
default:
break;
}
}
void MacroConditionScene::SetType(const Type &type)
{
_type = type;
SetupTempVars();
}
static inline void populateTypeSelection(QComboBox *list)
{
for (const auto &[id, name] : sceneTypes) {
@@ -268,8 +313,8 @@ void MacroConditionSceneEdit::TypeChanged(int index)
}
auto lock = LockContext();
_entryData->_type = static_cast<MacroConditionScene::Type>(
_sceneType->itemData(index).toInt());
_entryData->SetType(static_cast<MacroConditionScene::Type>(
_sceneType->itemData(index).toInt()));
SetWidgetVisibility();
}
@@ -296,31 +341,33 @@ void MacroConditionSceneEdit::UseTransitionTargetSceneChanged(int state)
void MacroConditionSceneEdit::SetWidgetVisibility()
{
_scenes->setVisible(
_entryData->_type == MacroConditionScene::Type::CURRENT ||
_entryData->_type == MacroConditionScene::Type::PREVIOUS ||
_entryData->_type == MacroConditionScene::Type::PREVIEW);
_entryData->GetType() == MacroConditionScene::Type::CURRENT ||
_entryData->GetType() == MacroConditionScene::Type::PREVIOUS ||
_entryData->GetType() == MacroConditionScene::Type::PREVIEW);
_useTransitionTargetScene->setVisible(
_entryData->_type == MacroConditionScene::Type::CURRENT ||
_entryData->_type == MacroConditionScene::Type::PREVIOUS ||
_entryData->_type ==
_entryData->GetType() == MacroConditionScene::Type::CURRENT ||
_entryData->GetType() == MacroConditionScene::Type::PREVIOUS ||
_entryData->GetType() ==
MacroConditionScene::Type::CURRENT_PATTERN ||
_entryData->_type ==
_entryData->GetType() ==
MacroConditionScene::Type::PREVIOUS_PATTERN);
_pattern->setVisible(
_entryData->_type ==
_entryData->GetType() ==
MacroConditionScene::Type::CURRENT_PATTERN ||
_entryData->_type ==
_entryData->GetType() ==
MacroConditionScene::Type::PREVIOUS_PATTERN ||
_entryData->_type ==
_entryData->GetType() ==
MacroConditionScene::Type::PREVIEW_PATTERN);
if (_entryData->_type == MacroConditionScene::Type::PREVIOUS ||
_entryData->_type == MacroConditionScene::Type::PREVIOUS_PATTERN) {
if (_entryData->GetType() == MacroConditionScene::Type::PREVIOUS ||
_entryData->GetType() ==
MacroConditionScene::Type::PREVIOUS_PATTERN) {
_useTransitionTargetScene->setText(obs_module_text(
"AdvSceneSwitcher.condition.scene.previousSceneTransitionBehaviour"));
}
if (_entryData->_type == MacroConditionScene::Type::CURRENT ||
_entryData->_type == MacroConditionScene::Type::CURRENT_PATTERN) {
if (_entryData->GetType() == MacroConditionScene::Type::CURRENT ||
_entryData->GetType() ==
MacroConditionScene::Type::CURRENT_PATTERN) {
_useTransitionTargetScene->setText(obs_module_text(
"AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour"));
}
@@ -336,7 +383,7 @@ void MacroConditionSceneEdit::UpdateEntryData()
_scenes->SetScene(_entryData->_scene);
_sceneType->setCurrentIndex(
_sceneType->findData(static_cast<int>(_entryData->_type)));
_sceneType->findData(static_cast<int>(_entryData->GetType())));
_pattern->setText(QString::fromStdString(_entryData->_pattern));
_useTransitionTargetScene->setChecked(
_entryData->_useTransitionTargetScene);

View File

@@ -32,9 +32,10 @@ public:
PREVIOUS_PATTERN = 70,
PREVIEW_PATTERN = 80,
};
void SetType(const Type &);
Type GetType() const { return _type; }
SceneSelection _scene;
Type _type = Type::CURRENT;
std::string _pattern = ".*Scene.*";
// During a transition "current" scene could either stand for the scene
// being transitioned to or the scene still being transitioned away
@@ -42,6 +43,9 @@ public:
bool _useTransitionTargetScene = false;
private:
void SetupTempVars();
Type _type = Type::CURRENT;
std::chrono::high_resolution_clock::time_point _lastSceneChangeTime{};
static bool _registered;
static const std::string id;

View File

@@ -0,0 +1,368 @@
#include "macro-condition-tempvar.hpp"
#include "math-helpers.hpp"
#include "utility.hpp"
namespace advss {
const std::string MacroConditionTempVar::id = "temp_var";
bool MacroConditionTempVar::_registered = MacroConditionFactory::Register(
MacroConditionTempVar::id,
{MacroConditionTempVar::Create, MacroConditionTempVarEdit::Create,
"AdvSceneSwitcher.condition.temporaryVariable"});
const static std::map<MacroConditionTempVar::Condition, std::string>
conditionTypes = {
{MacroConditionTempVar::Condition::EQUALS,
"AdvSceneSwitcher.condition.variable.type.compare"},
{MacroConditionTempVar::Condition::IS_EMPTY,
"AdvSceneSwitcher.condition.variable.type.empty"},
{MacroConditionTempVar::Condition::IS_NUMBER,
"AdvSceneSwitcher.condition.variable.type.number"},
{MacroConditionTempVar::Condition::LESS_THAN,
"AdvSceneSwitcher.condition.variable.type.lessThan"},
{MacroConditionTempVar::Condition::GREATER_THAN,
"AdvSceneSwitcher.condition.variable.type.greaterThan"},
{MacroConditionTempVar::Condition::VALUE_CHANGED,
"AdvSceneSwitcher.condition.variable.type.valueChanged"},
{MacroConditionTempVar::Condition::EQUALS_VARIABLE,
"AdvSceneSwitcher.condition.variable.type.equalsVariable"},
{MacroConditionTempVar::Condition::LESS_THAN_VARIABLE,
"AdvSceneSwitcher.condition.variable.type.lessThanVariable"},
{MacroConditionTempVar::Condition::GREATER_THAN_VARIABLE,
"AdvSceneSwitcher.condition.variable.type.greaterThanVariable"},
};
static bool isNumber(const TempVariable &var)
{
auto value = var.Value();
if (!value) {
return false;
}
return GetDouble(*value).has_value();
}
static bool compareNumber(const TempVariable &var, double value, bool less)
{
auto tempVarValue = var.Value();
if (!tempVarValue) {
return false;
}
auto doubleValue = GetDouble(*tempVarValue);
if (!doubleValue.has_value()) {
return false;
}
if (less) {
return doubleValue < value;
}
return doubleValue > value;
}
bool MacroConditionTempVar::Compare(const TempVariable &var) const
{
auto value = var.Value();
if (!value) {
return false;
}
if (_regex.Enabled()) {
auto expr = _regex.GetRegularExpression(_strValue);
if (!expr.isValid()) {
return false;
}
auto match = expr.match(QString::fromStdString(*value));
return match.hasMatch();
}
return std::string(_strValue) == *value;
}
bool MacroConditionTempVar::ValueChanged(const TempVariable &var)
{
auto value = var.Value();
if (!value) {
return false;
}
bool changed = *value != _lastValue;
if (changed) {
_lastValue = *value;
}
return changed;
}
bool MacroConditionTempVar::CompareVariables()
{
auto var1 = _tempVar.GetTempVariable(GetMacro());
auto var2 = _variable2.lock();
if (!var1 || !var2) {
return false;
}
auto tempVarValue = var1->Value();
if (!tempVarValue) {
return false;
}
auto val1 = GetDouble(*tempVarValue);
auto val2 = var2->DoubleValue();
bool validNumbers = val1.has_value() && val2.has_value();
switch (_type) {
case MacroConditionTempVar::Condition::EQUALS_VARIABLE:
return *tempVarValue == var2->Value() ||
(validNumbers && val1 == val2);
case MacroConditionTempVar::Condition::LESS_THAN_VARIABLE:
return validNumbers && val1 < val2;
case MacroConditionTempVar::Condition::GREATER_THAN_VARIABLE:
return validNumbers && val1 > val2;
default:
blog(LOG_WARNING,
"Unexpected call of %s with condition type %d", __func__,
static_cast<int>(_type));
}
return false;
}
bool MacroConditionTempVar::CheckCondition()
{
auto var = _tempVar.GetTempVariable(GetMacro());
if (!var) {
return false;
}
switch (_type) {
case MacroConditionTempVar::Condition::EQUALS:
return Compare(*var);
case MacroConditionTempVar::Condition::IS_EMPTY:
return var->Value().has_value() && var->Value()->empty();
case MacroConditionTempVar::Condition::IS_NUMBER:
return isNumber(*var);
case MacroConditionTempVar::Condition::LESS_THAN:
return compareNumber(*var, _numValue, true);
case MacroConditionTempVar::Condition::GREATER_THAN:
return compareNumber(*var, _numValue, false);
case MacroConditionTempVar::Condition::VALUE_CHANGED:
return ValueChanged(*var);
case MacroConditionTempVar::Condition::EQUALS_VARIABLE:
return CompareVariables();
case MacroConditionTempVar::Condition::LESS_THAN_VARIABLE:
return CompareVariables();
case MacroConditionTempVar::Condition::GREATER_THAN_VARIABLE:
return CompareVariables();
}
return false;
}
bool MacroConditionTempVar::Save(obs_data_t *obj) const
{
MacroCondition::Save(obj);
_tempVar.Save(obj);
obs_data_set_string(obj, "variableName",
GetWeakVariableName(_variable2).c_str());
_strValue.Save(obj, "strValue");
obs_data_set_double(obj, "numValue", _numValue);
obs_data_set_int(obj, "condition", static_cast<int>(_type));
_regex.Save(obj);
return true;
}
bool MacroConditionTempVar::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_tempVar.Load(obj, GetMacro());
_variable2 =
GetWeakVariableByName(obs_data_get_string(obj, "variableName"));
_strValue.Load(obj, "strValue");
_numValue = obs_data_get_double(obj, "numValue");
_type = static_cast<Condition>(obs_data_get_int(obj, "condition"));
_regex.Load(obj);
// TODO: remove in future version
if (obs_data_has_user_value(obj, "regex")) {
_regex.CreateBackwardsCompatibleRegex(
obs_data_get_bool(obj, "regex"));
}
return true;
}
std::string MacroConditionTempVar::GetShortDesc() const
{
auto var = _tempVar.GetTempVariable(GetMacro());
if (!var) {
return "";
}
return var->Name();
}
static inline void populateConditionSelection(QComboBox *list)
{
for (auto entry : conditionTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
}
}
MacroConditionTempVarEdit::MacroConditionTempVarEdit(
QWidget *parent, std::shared_ptr<MacroConditionTempVar> entryData)
: QWidget(parent),
_tempVars(new TempVariableSelection(this)),
_variables2(new VariableSelection(this)),
_conditions(new QComboBox()),
_strValue(new VariableTextEdit(this, 5, 1, 1)),
_numValue(new QDoubleSpinBox()),
_regex(new RegexConfigWidget(parent))
{
_numValue->setMinimum(-9999999999);
_numValue->setMaximum(9999999999);
populateConditionSelection(_conditions);
QWidget::connect(_tempVars,
SIGNAL(SelectionChanged(const TempVariableRef &)),
this, SLOT(VariableChanged(const TempVariableRef &)));
QWidget::connect(_variables2, SIGNAL(SelectionChanged(const QString &)),
this, SLOT(Variable2Changed(const QString &)));
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ConditionChanged(int)));
QWidget::connect(_strValue, SIGNAL(textChanged()), this,
SLOT(StrValueChanged()));
QWidget::connect(_numValue, SIGNAL(valueChanged(double)), this,
SLOT(NumValueChanged(double)));
QWidget::connect(_regex, SIGNAL(RegexConfigChanged(RegexConfig)), this,
SLOT(RegexChanged(RegexConfig)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{variables}}", _tempVars},
{"{{variables2}}", _variables2},
{"{{conditions}}", _conditions},
{"{{strValue}}", _strValue},
{"{{numValue}}", _numValue},
{"{{regex}}", _regex},
};
auto layout = new QHBoxLayout;
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.variable.entry"),
layout, widgetPlaceholders);
setLayout(layout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroConditionTempVarEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_tempVars->SetVariable(_entryData->_tempVar);
_variables2->SetVariable(_entryData->_variable2);
_conditions->setCurrentIndex(static_cast<int>(_entryData->_type));
_strValue->setPlainText(_entryData->_strValue);
_numValue->setValue(_entryData->_numValue);
_regex->SetRegexConfig(_entryData->_regex);
SetWidgetVisibility();
}
void MacroConditionTempVarEdit::VariableChanged(const TempVariableRef &var)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_tempVar = var;
}
void MacroConditionTempVarEdit::Variable2Changed(const QString &text)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_variable2 = GetWeakVariableByQString(text);
}
void MacroConditionTempVarEdit::ConditionChanged(int value)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_type =
static_cast<MacroConditionTempVar::Condition>(value);
SetWidgetVisibility();
}
void MacroConditionTempVarEdit::StrValueChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_strValue = _strValue->toPlainText().toStdString();
adjustSize();
updateGeometry();
}
void MacroConditionTempVarEdit::NumValueChanged(double val)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_numValue = val;
}
void MacroConditionTempVarEdit::RegexChanged(RegexConfig conf)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_regex = conf;
adjustSize();
updateGeometry();
}
void MacroConditionTempVarEdit::SetWidgetVisibility()
{
if (!_entryData) {
return;
}
_regex->setVisible(_entryData->_type ==
MacroConditionTempVar::Condition::EQUALS);
_strValue->setVisible(_entryData->_type ==
MacroConditionTempVar::Condition::EQUALS);
_numValue->setVisible(
_entryData->_type ==
MacroConditionTempVar::Condition::LESS_THAN ||
_entryData->_type ==
MacroConditionTempVar::Condition::GREATER_THAN);
_numValue->setVisible(
_entryData->_type ==
MacroConditionTempVar::Condition::LESS_THAN ||
_entryData->_type ==
MacroConditionTempVar::Condition::GREATER_THAN);
_variables2->setVisible(
_entryData->_type ==
MacroConditionTempVar::Condition::EQUALS_VARIABLE ||
_entryData->_type ==
MacroConditionTempVar::Condition::LESS_THAN_VARIABLE ||
_entryData->_type ==
MacroConditionTempVar::Condition::GREATER_THAN_VARIABLE);
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@@ -0,0 +1,100 @@
#pragma once
#include "macro-condition-edit.hpp"
#include "resizing-text-edit.hpp"
#include "variable.hpp"
#include "regex-config.hpp"
#include "variable-text-edit.hpp"
#include <QWidget>
#include <QComboBox>
#include <QLineEdit>
#include <QCheckBox>
namespace advss {
class MacroConditionTempVar : public MacroCondition {
public:
MacroConditionTempVar(Macro *m) : MacroCondition(m) {}
bool CheckCondition();
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<MacroCondition> Create(Macro *m)
{
return std::make_shared<MacroConditionTempVar>(m);
}
enum class Condition {
EQUALS,
IS_EMPTY,
IS_NUMBER,
LESS_THAN,
GREATER_THAN,
VALUE_CHANGED,
EQUALS_VARIABLE,
LESS_THAN_VARIABLE,
GREATER_THAN_VARIABLE,
};
Condition _type = Condition::EQUALS;
TempVariableRef _tempVar;
std::weak_ptr<Variable> _variable2;
StringVariable _strValue = "";
double _numValue = 0;
RegexConfig _regex;
private:
bool Compare(const TempVariable &) const;
bool ValueChanged(const TempVariable &);
bool CompareVariables();
std::string _lastValue = "";
static bool _registered;
static const std::string id;
};
class MacroConditionTempVarEdit : public QWidget {
Q_OBJECT
public:
MacroConditionTempVarEdit(
QWidget *parent,
std::shared_ptr<MacroConditionTempVar> cond = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroCondition> cond)
{
return new MacroConditionTempVarEdit(
parent,
std::dynamic_pointer_cast<MacroConditionTempVar>(cond));
}
private slots:
void VariableChanged(const TempVariableRef &);
void Variable2Changed(const QString &);
void ConditionChanged(int);
void StrValueChanged();
void NumValueChanged(double);
void RegexChanged(RegexConfig);
signals:
void HeaderInfoChanged(const QString &);
protected:
TempVariableSelection *_tempVars;
VariableSelection *_variables2;
QComboBox *_conditions;
VariableTextEdit *_strValue;
QDoubleSpinBox *_numValue;
RegexConfigWidget *_regex;
std::shared_ptr<MacroConditionTempVar> _entryData;
private:
void SetWidgetVisibility();
bool _loading = true;
};
} // namespace advss

View File

@@ -10,18 +10,20 @@ bool MacroConditionTimer::_registered = MacroConditionFactory::Register(
{MacroConditionTimer::Create, MacroConditionTimerEdit::Create,
"AdvSceneSwitcher.condition.timer", false});
const static std::map<TimerType, std::string> timerTypes = {
{TimerType::FIXED, "AdvSceneSwitcher.condition.timer.type.fixed"},
{TimerType::RANDOM, "AdvSceneSwitcher.condition.timer.type.random"},
const static std::map<MacroConditionTimer::TimerType, std::string> timerTypes = {
{MacroConditionTimer::TimerType::FIXED,
"AdvSceneSwitcher.condition.timer.type.fixed"},
{MacroConditionTimer::TimerType::RANDOM,
"AdvSceneSwitcher.condition.timer.type.random"},
};
bool MacroConditionTimer::CheckCondition()
{
if (_paused) {
SetVariableValue(std::to_string(_remaining));
SetVariables(_remaining);
return _remaining == 0.;
}
SetVariableValue(std::to_string(_duration.TimeRemaining()));
SetVariables(_duration.TimeRemaining());
if (_duration.DurationReached()) {
if (!_oneshot) {
_duration.Reset();
@@ -120,6 +122,29 @@ void MacroConditionTimer::Reset()
}
}
void MacroConditionTimer::SetVariables(double seconds)
{
SetVariableValue(std::to_string(seconds));
SetTempVarValue("seconds", std::to_string((int)seconds));
SetTempVarValue("minutes", std::to_string((int)(seconds / 60)));
SetTempVarValue("hours", std::to_string((int)(seconds / (60 * 60))));
SetTempVarValue("days",
std::to_string((int)(seconds / (60 * 60 * 24))));
}
void MacroConditionTimer::SetupTempVars()
{
MacroCondition::SetupTempVars();
AddTempvar("seconds",
obs_module_text("AdvSceneSwitcher.tempVar.timer.seconds"));
AddTempvar("minutes",
obs_module_text("AdvSceneSwitcher.tempVar.timer.minutes"));
AddTempvar("hours",
obs_module_text("AdvSceneSwitcher.tempVar.timer.hours"));
AddTempvar("days",
obs_module_text("AdvSceneSwitcher.tempVar.timer.days"));
}
static inline void populateTimerTypeSelection(QComboBox *list)
{
for (auto entry : timerTypes) {
@@ -203,7 +228,7 @@ void MacroConditionTimerEdit::TimerTypeChanged(int type)
}
auto lock = LockContext();
_entryData->_type = static_cast<TimerType>(type);
_entryData->_type = static_cast<MacroConditionTimer::TimerType>(type);
SetWidgetVisibility();
}
@@ -337,7 +362,7 @@ void MacroConditionTimerEdit::SetWidgetVisibility()
{"{{duration2}}", _duration2},
};
if (_entryData->_type == TimerType::RANDOM) {
if (_entryData->_type == MacroConditionTimer::TimerType::RANDOM) {
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.timer.entry.line1.random"),

View File

@@ -12,11 +12,6 @@
namespace advss {
enum class TimerType {
FIXED,
RANDOM,
};
class MacroConditionTimer : public MacroCondition {
public:
MacroConditionTimer(Macro *m) : MacroCondition(m, true) {}
@@ -32,6 +27,7 @@ public:
void Continue();
void Reset();
enum class TimerType { FIXED, RANDOM };
TimerType _type = TimerType::FIXED;
Duration _duration;
Duration _duration2;
@@ -42,6 +38,9 @@ public:
private:
void SetRandomTimeRemaining();
void SetVariables(double seconds);
void SetupTempVars();
std::default_random_engine _re;
static bool _registered;
static const std::string id;

View File

@@ -1,8 +1,6 @@
#include "macro-condition-variable.hpp"
#include "utility.hpp"
#include <regex>
namespace advss {
const std::string MacroConditionVariable::id = "variable";
@@ -12,25 +10,25 @@ bool MacroConditionVariable::_registered = MacroConditionFactory::Register(
{MacroConditionVariable::Create, MacroConditionVariableEdit::Create,
"AdvSceneSwitcher.condition.variable"});
const static std::map<MacroConditionVariable::Type, std::string>
const static std::map<MacroConditionVariable::Condition, std::string>
conditionTypes = {
{MacroConditionVariable::Type::EQUALS,
{MacroConditionVariable::Condition::EQUALS,
"AdvSceneSwitcher.condition.variable.type.compare"},
{MacroConditionVariable::Type::IS_EMPTY,
{MacroConditionVariable::Condition::IS_EMPTY,
"AdvSceneSwitcher.condition.variable.type.empty"},
{MacroConditionVariable::Type::IS_NUMBER,
{MacroConditionVariable::Condition::IS_NUMBER,
"AdvSceneSwitcher.condition.variable.type.number"},
{MacroConditionVariable::Type::LESS_THAN,
{MacroConditionVariable::Condition::LESS_THAN,
"AdvSceneSwitcher.condition.variable.type.lessThan"},
{MacroConditionVariable::Type::GREATER_THAN,
{MacroConditionVariable::Condition::GREATER_THAN,
"AdvSceneSwitcher.condition.variable.type.greaterThan"},
{MacroConditionVariable::Type::VALUE_CHANGED,
{MacroConditionVariable::Condition::VALUE_CHANGED,
"AdvSceneSwitcher.condition.variable.type.valueChanged"},
{MacroConditionVariable::Type::EQUALS_VARIABLE,
{MacroConditionVariable::Condition::EQUALS_VARIABLE,
"AdvSceneSwitcher.condition.variable.type.equalsVariable"},
{MacroConditionVariable::Type::LESS_THAN_VARIABLE,
{MacroConditionVariable::Condition::LESS_THAN_VARIABLE,
"AdvSceneSwitcher.condition.variable.type.lessThanVariable"},
{MacroConditionVariable::Type::GREATER_THAN_VARIABLE,
{MacroConditionVariable::Condition::GREATER_THAN_VARIABLE,
"AdvSceneSwitcher.condition.variable.type.greaterThanVariable"},
};
@@ -62,7 +60,7 @@ bool MacroConditionVariable::Compare(const Variable &var) const
return match.hasMatch();
}
return _strValue == var.Value();
return std::string(_strValue) == var.Value();
}
bool MacroConditionVariable::ValueChanged(const Variable &var)
@@ -87,12 +85,12 @@ bool MacroConditionVariable::CompareVariables()
bool validNumbers = val1.has_value() && val2.has_value();
switch (_type) {
case MacroConditionVariable::Type::EQUALS_VARIABLE:
case MacroConditionVariable::Condition::EQUALS_VARIABLE:
return var1->Value() == var2->Value() ||
(validNumbers && val1 == val2);
case MacroConditionVariable::Type::LESS_THAN_VARIABLE:
case MacroConditionVariable::Condition::LESS_THAN_VARIABLE:
return validNumbers && val1 < val2;
case MacroConditionVariable::Type::GREATER_THAN_VARIABLE:
case MacroConditionVariable::Condition::GREATER_THAN_VARIABLE:
return validNumbers && val1 > val2;
default:
blog(LOG_WARNING,
@@ -111,23 +109,23 @@ bool MacroConditionVariable::CheckCondition()
}
switch (_type) {
case MacroConditionVariable::Type::EQUALS:
case MacroConditionVariable::Condition::EQUALS:
return Compare(*var);
case MacroConditionVariable::Type::IS_EMPTY:
case MacroConditionVariable::Condition::IS_EMPTY:
return var->Value().empty();
case MacroConditionVariable::Type::IS_NUMBER:
case MacroConditionVariable::Condition::IS_NUMBER:
return isNumber(*var);
case MacroConditionVariable::Type::LESS_THAN:
case MacroConditionVariable::Condition::LESS_THAN:
return compareNumber(*var, _numValue, true);
case MacroConditionVariable::Type::GREATER_THAN:
case MacroConditionVariable::Condition::GREATER_THAN:
return compareNumber(*var, _numValue, false);
case MacroConditionVariable::Type::VALUE_CHANGED:
case MacroConditionVariable::Condition::VALUE_CHANGED:
return ValueChanged(*var);
case MacroConditionVariable::Type::EQUALS_VARIABLE:
case MacroConditionVariable::Condition::EQUALS_VARIABLE:
return CompareVariables();
case MacroConditionVariable::Type::LESS_THAN_VARIABLE:
case MacroConditionVariable::Condition::LESS_THAN_VARIABLE:
return CompareVariables();
case MacroConditionVariable::Type::GREATER_THAN_VARIABLE:
case MacroConditionVariable::Condition::GREATER_THAN_VARIABLE:
return CompareVariables();
}
@@ -141,7 +139,7 @@ bool MacroConditionVariable::Save(obs_data_t *obj) const
GetWeakVariableName(_variable).c_str());
obs_data_set_string(obj, "variable2Name",
GetWeakVariableName(_variable2).c_str());
obs_data_set_string(obj, "strValue", _strValue.c_str());
_strValue.Save(obj, "strValue");
obs_data_set_double(obj, "numValue", _numValue);
obs_data_set_int(obj, "condition", static_cast<int>(_type));
_regex.Save(obj);
@@ -155,9 +153,9 @@ bool MacroConditionVariable::Load(obs_data_t *obj)
GetWeakVariableByName(obs_data_get_string(obj, "variableName"));
_variable2 = GetWeakVariableByName(
obs_data_get_string(obj, "variable2Name"));
_strValue = obs_data_get_string(obj, "strValue");
_strValue.Load(obj, "strValue");
_numValue = obs_data_get_double(obj, "numValue");
_type = static_cast<Type>(obs_data_get_int(obj, "condition"));
_type = static_cast<Condition>(obs_data_get_int(obj, "condition"));
_regex.Load(obj);
// TODO: remove in future version
if (obs_data_has_user_value(obj, "regex")) {
@@ -185,7 +183,7 @@ MacroConditionVariableEdit::MacroConditionVariableEdit(
_variables(new VariableSelection(this)),
_variables2(new VariableSelection(this)),
_conditions(new QComboBox()),
_strValue(new ResizingPlainTextEdit(this, 5, 1, 1)),
_strValue(new VariableTextEdit(this, 5, 1, 1)),
_numValue(new QDoubleSpinBox()),
_regex(new RegexConfigWidget(parent))
{
@@ -215,19 +213,11 @@ MacroConditionVariableEdit::MacroConditionVariableEdit(
{"{{regex}}", _regex},
};
QHBoxLayout *layout = new QHBoxLayout;
auto layout = new QHBoxLayout;
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.variable.entry"),
layout, widgetPlaceholders);
auto *regexLayout = new QHBoxLayout;
regexLayout->addWidget(_regex);
regexLayout->addStretch();
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(layout);
mainLayout->addLayout(regexLayout);
setLayout(mainLayout);
setLayout(layout);
_entryData = entryData;
UpdateEntryData();
@@ -243,7 +233,7 @@ void MacroConditionVariableEdit::UpdateEntryData()
_variables->SetVariable(_entryData->_variable);
_variables2->SetVariable(_entryData->_variable2);
_conditions->setCurrentIndex(static_cast<int>(_entryData->_type));
_strValue->setPlainText(QString::fromStdString(_entryData->_strValue));
_strValue->setPlainText(_entryData->_strValue);
_numValue->setValue(_entryData->_numValue);
_regex->SetRegexConfig(_entryData->_regex);
SetWidgetVisibility();
@@ -277,7 +267,8 @@ void MacroConditionVariableEdit::ConditionChanged(int value)
}
auto lock = LockContext();
_entryData->_type = static_cast<MacroConditionVariable::Type>(value);
_entryData->_type =
static_cast<MacroConditionVariable::Condition>(value);
SetWidgetVisibility();
}
@@ -290,6 +281,7 @@ void MacroConditionVariableEdit::StrValueChanged()
auto lock = LockContext();
_entryData->_strValue = _strValue->toPlainText().toStdString();
adjustSize();
updateGeometry();
}
void MacroConditionVariableEdit::NumValueChanged(double val)
@@ -321,24 +313,26 @@ void MacroConditionVariableEdit::SetWidgetVisibility()
}
_regex->setVisible(_entryData->_type ==
MacroConditionVariable::Type::EQUALS);
MacroConditionVariable::Condition::EQUALS);
_strValue->setVisible(_entryData->_type ==
MacroConditionVariable::Type::EQUALS);
MacroConditionVariable::Condition::EQUALS);
_numValue->setVisible(
_entryData->_type == MacroConditionVariable::Type::LESS_THAN ||
_entryData->_type ==
MacroConditionVariable::Type::GREATER_THAN);
MacroConditionVariable::Condition::LESS_THAN ||
_entryData->_type ==
MacroConditionVariable::Condition::GREATER_THAN);
_numValue->setVisible(
_entryData->_type == MacroConditionVariable::Type::LESS_THAN ||
_entryData->_type ==
MacroConditionVariable::Type::GREATER_THAN);
MacroConditionVariable::Condition::LESS_THAN ||
_entryData->_type ==
MacroConditionVariable::Condition::GREATER_THAN);
_variables2->setVisible(
_entryData->_type ==
MacroConditionVariable::Type::EQUALS_VARIABLE ||
MacroConditionVariable::Condition::EQUALS_VARIABLE ||
_entryData->_type ==
MacroConditionVariable::Type::LESS_THAN_VARIABLE ||
MacroConditionVariable::Condition::LESS_THAN_VARIABLE ||
_entryData->_type ==
MacroConditionVariable::Type::GREATER_THAN_VARIABLE);
MacroConditionVariable::Condition::GREATER_THAN_VARIABLE);
adjustSize();
updateGeometry();

View File

@@ -3,6 +3,7 @@
#include "resizing-text-edit.hpp"
#include "variable.hpp"
#include "regex-config.hpp"
#include "variable-text-edit.hpp"
#include <QWidget>
#include <QComboBox>
@@ -24,7 +25,7 @@ public:
return std::make_shared<MacroConditionVariable>(m);
}
enum class Type {
enum class Condition {
EQUALS,
IS_EMPTY,
IS_NUMBER,
@@ -36,10 +37,10 @@ public:
GREATER_THAN_VARIABLE,
};
Type _type = Type::EQUALS;
Condition _type = Condition::EQUALS;
std::weak_ptr<Variable> _variable;
std::weak_ptr<Variable> _variable2;
std::string _strValue = "";
StringVariable _strValue = "";
double _numValue = 0;
RegexConfig _regex;
@@ -86,7 +87,7 @@ protected:
VariableSelection *_variables;
VariableSelection *_variables2;
QComboBox *_conditions;
ResizingPlainTextEdit *_strValue;
VariableTextEdit *_strValue;
QDoubleSpinBox *_numValue;
RegexConfigWidget *_regex;
std::shared_ptr<MacroConditionVariable> _entryData;

View File

@@ -34,28 +34,24 @@ static bool matchRegex(const RegexConfig &conf, const std::string &msg,
bool MacroConditionWebsocket::CheckCondition()
{
const std::vector<std::string> *messages = nullptr;
std::vector<std::string> messages;
switch (_type) {
case MacroConditionWebsocket::Type::REQUEST:
messages = &switcher->websocketMessages;
messages = GetWebsocketMessages();
break;
case MacroConditionWebsocket::Type::EVENT: {
auto connection = _connection.lock();
if (!connection) {
return false;
}
messages = &connection->Events();
messages = connection->Events();
break;
}
default:
break;
}
if (!messages) {
return false;
}
for (const auto &msg : *messages) {
for (const auto &msg : messages) {
if (_regex.Enabled()) {
if (matchRegex(_regex, msg, _message)) {
SetVariableValue(msg);

View File

@@ -96,6 +96,7 @@ bool MacroConditionWindow::WindowRegexMatches(
void MacroConditionWindow::SetVariableValueBasedOnMatch(
const std::string &matchWindow)
{
SetTempVarValue("window", matchWindow);
if (!IsReferencedInVars()) {
return;
}
@@ -173,6 +174,16 @@ std::string MacroConditionWindow::GetShortDesc() const
return _window;
}
void MacroConditionWindow::SetupTempVars()
{
MacroCondition::SetupTempVars();
AddTempvar(
"window",
obs_module_text("AdvSceneSwitcher.tempVar.window.window"),
obs_module_text(
"AdvSceneSwitcher.tempVar.window.window.description"));
}
MacroConditionWindowEdit::MacroConditionWindowEdit(
QWidget *parent, std::shared_ptr<MacroConditionWindow> entryData)
: QWidget(parent),

View File

@@ -21,7 +21,6 @@ public:
return std::make_shared<MacroConditionWindow>(m);
}
public:
StringVariable _window;
RegexConfig _windowRegex;
bool _checkTitle = true;
@@ -40,6 +39,7 @@ private:
bool WindowMatches(const std::vector<std::string> &windowList);
bool WindowRegexMatches(const std::vector<std::string> &windowList);
void SetVariableValueBasedOnMatch(const std::string &matchWindow);
void SetupTempVars();
static bool _registered;
static const std::string id;

View File

@@ -22,7 +22,7 @@ void DurationModifier::Save(obs_data_t *obj, const char *condName,
void DurationModifier::Load(obs_data_t *obj, const char *condName,
const char *duration)
{
// For backwards compatability check if duration value exist without
// For backwards compatibility check if duration value exist without
// time constraint condition - if so assume DurationCondition::MORE
if (!obs_data_has_user_value(obj, condName) &&
obs_data_has_user_value(obj, duration)) {
@@ -105,7 +105,7 @@ bool MacroCondition::Load(obs_data_t *obj)
_duration.Load(durObj);
obs_data_release(durObj);
} else {
// For backwards compatability
// For backwards compatibility
_duration.Load(obj);
}
return true;

View File

@@ -1,4 +1,5 @@
#include "macro-segment.hpp"
#include "macro.hpp"
#include "section.hpp"
#include "utility.hpp"
#include "mouse-wheel-guard.hpp"
@@ -26,11 +27,13 @@ bool MacroSegment::Save(obs_data_t *obj) const
bool MacroSegment::Load(obs_data_t *obj)
{
_collapsed = obs_data_get_bool(obj, "collapsed");
ClearAvailableTempvars();
return true;
}
bool MacroSegment::PostLoad()
{
SetupTempVars();
return true;
}
@@ -68,20 +71,110 @@ void MacroSegment::SetVariableValue(const std::string &value)
}
}
void MacroSegment::IncrementVariableRef()
void MacroSegment::SetupTempVars()
{
if (!_supportsVariableValue) {
return;
}
_variableRefs++;
ClearAvailableTempvars();
}
void MacroSegment::DecrementVariableRef()
void MacroSegment::ClearAvailableTempvars()
{
if (!_supportsVariableValue || _variableRefs == 0) {
_tempVariables.clear();
NotifyUIAboutTempVarChange();
}
static std::shared_ptr<MacroSegment>
getSharedSegmentFromMacro(Macro *macro, const MacroSegment *segment)
{
auto matches = [segment](const std::shared_ptr<MacroSegment> &s) {
return s.get() == segment;
};
auto condIt = std::find_if(macro->Conditions().begin(),
macro->Conditions().end(), matches);
if (condIt != macro->Conditions().end()) {
return *condIt;
}
auto actionIt = std::find_if(macro->Actions().begin(),
macro->Actions().end(), matches);
if (actionIt != macro->Actions().end()) {
return *actionIt;
}
auto elseIt = std::find_if(macro->ElseActions().begin(),
macro->ElseActions().end(), matches);
if (elseIt != macro->ElseActions().end()) {
return *elseIt;
}
return {};
}
void MacroSegment::AddTempvar(const std::string &id, const std::string &name,
const std::string &description)
{
auto macro = GetMacro();
if (!macro) {
return;
}
_variableRefs--;
auto sharedSegment = getSharedSegmentFromMacro(macro, this);
if (!sharedSegment) {
return;
}
TempVariable var(id, name, description, sharedSegment);
_tempVariables.emplace_back(std::move(var));
NotifyUIAboutTempVarChange();
}
void MacroSegment::SetTempVarValue(const std::string &id,
const std::string &value)
{
for (auto &var : _tempVariables) {
if (var.ID() != id) {
continue;
}
var.SetValue(value);
break;
}
}
void MacroSegment::InvalidateTempVarValues()
{
for (auto &var : _tempVariables) {
var.InvalidateValue();
}
}
std::optional<const TempVariable>
MacroSegment::GetTempVar(const std::string &id) const
{
for (auto &var : _tempVariables) {
if (var.ID() == id) {
return var;
}
}
vblog(LOG_INFO, "cannot get value of unknown tempvar %s", id.c_str());
return {};
}
bool SupportsVariableValue(MacroSegment *segment)
{
return segment && segment->_supportsVariableValue;
}
void IncrementVariableRef(MacroSegment *segment)
{
if (!segment || !segment->_supportsVariableValue) {
return;
}
segment->_variableRefs++;
}
void DecrementVariableRef(MacroSegment *segment)
{
if (!segment || !segment->_supportsVariableValue ||
segment->_variableRefs == 0) {
return;
}
segment->_variableRefs--;
}
MacroSegmentEdit::MacroSegmentEdit(bool highlight, QWidget *parent)

View File

@@ -1,10 +1,11 @@
#pragma once
// The following helpers are used by all macro seements,
// The following helpers are used by all macro segments,
// so it makes sense to include them here:
#include "log-helper.hpp"
#include "obs-module-helper.hpp"
#include "sync-helper.hpp"
#include "temp-variable.hpp"
#include <QWidget>
#include <QFrame>
@@ -34,16 +35,26 @@ public:
virtual std::string GetId() const = 0;
void SetHighlight();
bool Highlight();
bool SupportsVariableValue() const { return _supportsVariableValue; }
virtual std::string GetVariableValue() const;
void IncrementVariableRef();
void DecrementVariableRef();
protected:
friend bool SupportsVariableValue(MacroSegment *);
friend void IncrementVariableRef(MacroSegment *);
friend void DecrementVariableRef(MacroSegment *);
void SetVariableValue(const std::string &value);
bool IsReferencedInVars() { return _variableRefs != 0; }
virtual void SetupTempVars();
void AddTempvar(const std::string &id, const std::string &name,
const std::string &description = "");
void SetTempVarValue(const std::string &id, const std::string &value);
private:
void ClearAvailableTempvars();
std::optional<const TempVariable>
GetTempVar(const std::string &id) const;
void InvalidateTempVarValues();
// Macro helpers
Macro *_macro = nullptr;
int _idx = 0;
@@ -56,6 +67,9 @@ private:
const bool _supportsVariableValue = false;
int _variableRefs = 0;
std::string _variableValue;
std::vector<TempVariable> _tempVariables;
friend class Macro;
};
class Section;
@@ -70,6 +84,7 @@ public:
void SetFocusPolicyOfWidgets();
void SetCollapsed(bool collapsed);
void SetSelected(bool);
virtual std::shared_ptr<MacroSegment> Data() const = 0;
protected slots:
void HeaderInfoChanged(const QString &);
@@ -99,7 +114,6 @@ private:
BELOW,
};
virtual std::shared_ptr<MacroSegment> Data() = 0;
void ShowDropLine(DropLineState);
// The reason for using two separate frame widget each with their own

View File

@@ -235,20 +235,18 @@ void AdvSceneSwitcher::ExportMacros()
}
}
auto data = obs_data_create();
auto macroArray = obs_data_array_create();
OBSDataAutoRelease data = obs_data_create();
OBSDataArrayAutoRelease macroArray = obs_data_array_create();
for (const auto &macro : macros) {
obs_data_t *obj = obs_data_create();
OBSDataAutoRelease obj = obs_data_create();
macro->Save(obj);
obs_data_array_push_back(macroArray, obj);
obs_data_release(obj);
}
obs_data_set_array(data, "macros", macroArray);
obs_data_array_release(macroArray);
switcher->SaveVariables(data);
obs_data_set_string(data, "version", g_GIT_TAG);
auto json = obs_data_get_json(data);
QString exportString(json);
obs_data_release(data);
MacroExportImportDialog::ExportMacros(exportString);
}
@@ -267,8 +265,9 @@ void AdvSceneSwitcher::SetupMacroSegmentSelection(MacroSection type, int idx)
return;
}
MacroSegmentList *setList, *resetList1, *resetList2;
int *setIdx, *resetIdx1, *resetIdx2;
MacroSegmentList *setList = nullptr, *resetList1 = nullptr,
*resetList2 = nullptr;
int *setIdx = nullptr, *resetIdx1 = nullptr, *resetIdx2 = nullptr;
std::deque<std::shared_ptr<MacroSegment>> segements;
switch (type) {
@@ -367,19 +366,41 @@ bool AdvSceneSwitcher::ResolveMacroImportNameConflict(
return true;
}
static bool variableWithNameExists(const std::string &name)
{
return !!GetVariableByName(name);
}
static void importVariables(obs_data_t *obj)
{
OBSDataArrayAutoRelease array = obs_data_get_array(obj, "variables");
size_t count = obs_data_array_count(array);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease data = obs_data_array_item(array, i);
auto var = Variable::Create();
var->Load(data);
if (variableWithNameExists(var->Name())) {
continue;
}
switcher->variables.emplace_back(var);
}
}
void AdvSceneSwitcher::ImportMacros()
{
QString json;
if (!MacroExportImportDialog::ImportMacros(json)) {
return;
}
auto data = obs_data_create_from_json(json.toStdString().c_str());
OBSDataAutoRelease data =
obs_data_create_from_json(json.toStdString().c_str());
if (!data) {
DisplayMessage(obs_module_text(
"AdvSceneSwitcher.macroTab.import.invalid"));
ImportMacros();
return;
}
importVariables(data);
auto version = obs_data_get_string(data, "version");
if (strcmp(version, g_GIT_TAG) != 0) {
@@ -388,27 +409,26 @@ void AdvSceneSwitcher::ImportMacros()
version);
}
auto array = obs_data_get_array(data, "macros");
OBSDataArrayAutoRelease array = obs_data_get_array(data, "macros");
size_t count = obs_data_array_count(array);
int groupSize = 0;
std::shared_ptr<Macro> group;
std::vector<std::shared_ptr<Macro>> importedMacros;
auto lock = LockContext();
for (size_t i = 0; i < count; i++) {
obs_data_t *array_obj = obs_data_array_item(array, i);
OBSDataAutoRelease array_obj = obs_data_array_item(array, i);
auto macro = std::make_shared<Macro>();
macro->Load(array_obj);
macro->PostLoad();
if (macroNameExists(macro->Name()) &&
!ResolveMacroImportNameConflict(macro)) {
obs_data_release(array_obj);
groupSize--;
continue;
}
importedMacros.emplace_back(macro);
switcher->macros.emplace_back(macro);
if (groupSize > 0 && !macro->IsGroup()) {
Macro::PrepareMoveToGroup(group, macro);
@@ -423,11 +443,11 @@ void AdvSceneSwitcher::ImportMacros()
// to the group as they come up.
macro->ResetGroupSize();
}
obs_data_release(array_obj);
}
obs_data_array_release(array);
obs_data_release(data);
for (const auto &macro : importedMacros) {
macro->PostLoad();
}
ui->macros->Reset(switcher->macros,
switcher->macroProperties._highlightExecuted);
@@ -678,8 +698,7 @@ void AdvSceneSwitcher::MacroSelectionAboutToChange()
// actions nor elseActions being visible when the condition <-> action
// splitter is moved
if (elsePos[0] == 0 && elsePos[1] == 0) {
macro->SetElseActionSplitterPosition(QList<int>()
<< 999999 << 0);
maximizeFirstSplitterEntry(ui->macroElseActionSplitter);
return;
}
macro->SetElseActionSplitterPosition(
@@ -768,6 +787,8 @@ bool shouldRestoreSplitter(const QList<int> &pos)
void AdvSceneSwitcher::SetupMacroTab()
{
ui->macroElseActions->installEventFilter(this);
if (switcher->macros.size() == 0 && !switcher->disableHints) {
addPulse = PulseWidget(ui->macroAdd, QColor(Qt::green));
}
@@ -840,6 +861,8 @@ void AdvSceneSwitcher::SetupMacroTab()
SetButtonIcon(ui->conditionTop, (pathPrefix + "DoubleUp.svg").c_str());
SetButtonIcon(ui->conditionBottom,
(pathPrefix + "DoubleDown.svg").c_str());
SetButtonIcon(ui->toggleElseActions,
(pathPrefix + "NotEqual.svg").c_str());
// Reserve more space for macro edit area than for the macro list
ui->macroListMacroEditSplitter->setStretchFactor(0, 1);
@@ -966,13 +989,12 @@ void AdvSceneSwitcher::CopyMacro()
return;
}
obs_data_t *data = obs_data_create();
OBSDataAutoRelease data = obs_data_create();
macro->Save(data);
newMacro->Load(data);
newMacro->PostLoad();
newMacro->SetName(name);
Macro::PrepareMoveToGroup(macro->Parent(), newMacro);
obs_data_release(data);
ui->macros->Add(newMacro, macro);
ui->macroAdd->disconnect(addPulse);
@@ -1078,6 +1100,31 @@ void AdvSceneSwitcher::MaximizeConditions()
MinimizeActions();
}
void AdvSceneSwitcher::on_toggleElseActions_clicked()
{
auto elsePosition = ui->macroElseActionSplitter->sizes();
if (elsePosition[1] == 0) {
centerSplitterPosition(ui->macroElseActionSplitter);
return;
}
maximizeFirstSplitterEntry(ui->macroElseActionSplitter);
}
void AdvSceneSwitcher::SetElseActionsStateToHidden()
{
ui->toggleElseActions->setToolTip(obs_module_text(
"AdvSceneSwitcher.macroTab.toggleElseActions.show.tooltip"));
ui->toggleElseActions->setChecked(false);
}
void AdvSceneSwitcher::SetElseActionsStateToVisible()
{
ui->toggleElseActions->setToolTip(obs_module_text(
"AdvSceneSwitcher.macroTab.toggleElseActions.hide.tooltip"));
ui->toggleElseActions->setChecked(true);
}
bool AdvSceneSwitcher::MacroTabIsInFocus()
{
return isActiveWindow() && isAncestorOf(focusWidget()) &&

View File

@@ -212,7 +212,7 @@ static inline int
ModelIndexToMacroIndex(int modelIdx,
const std::deque<std::shared_ptr<Macro>> &macros)
{
assert(macros.size() >= modelIdx);
assert((int)macros.size() >= modelIdx);
int realIdx = 0;
const auto &m = macros[0];
bool inCollapsedGroup = m->IsGroup() && m->IsCollapsed();

View File

@@ -149,6 +149,7 @@ bool Macro::CeckMatch()
}
break;
case LogicType::OR_NOT:
_matched = _matched || !cond;
if (!cond) {
c->SetHighlight();
}
@@ -276,9 +277,13 @@ void Macro::ResetTimers()
}
bool Macro::RunActionsHelper(
const std::deque<std::shared_ptr<MacroAction>> &actions,
const std::deque<std::shared_ptr<MacroAction>> &actionsToRun,
bool ignorePause)
{
// Create copy of action list as elements might be removed, inserted, or
// reordered while actions are currently being executed.
auto actions = actionsToRun;
bool actionsExecutedSuccessfully = true;
for (auto &action : actions) {
if (action->Enabled()) {
@@ -357,6 +362,126 @@ void Macro::Stop()
}
}
std::vector<TempVariable> Macro::GetTempVars(MacroSegment *filter) const
{
std::vector<TempVariable> res;
auto addTempVars = [&res](const std::deque<std::shared_ptr<MacroSegment>>
&segments) {
for (const auto &s : segments) {
const auto &tempVars = s->_tempVariables;
res.insert(res.end(), tempVars.begin(), tempVars.end());
}
};
addTempVars({_conditions.begin(), _conditions.end()});
addTempVars({_actions.begin(), _actions.end()});
addTempVars({_elseActions.begin(), _elseActions.end()});
if (!filter) {
return res;
}
auto isCondition = [this](const MacroSegment *segment) -> bool {
return std::find_if(_conditions.begin(), _conditions.end(),
[segment](
const std::shared_ptr<MacroSegment>
&ptr) {
return ptr.get() == segment;
}) != _conditions.end();
};
auto isAction = [this](MacroSegment *segment) -> bool {
return std::find_if(_actions.begin(), _actions.end(),
[segment](
const std::shared_ptr<MacroSegment>
&ptr) {
return ptr.get() == segment;
}) != _actions.end();
};
auto isElseAction = [this](MacroSegment *segment) -> bool {
return std::find_if(_elseActions.begin(), _elseActions.end(),
[segment](
const std::shared_ptr<MacroSegment>
&ptr) {
return ptr.get() == segment;
}) != _elseActions.end();
};
const int filterIndex = filter->GetIndex();
// Remove all actions and else actions and conditions after filterIndex
if (isCondition(filter)) {
for (auto it = res.begin(); it != res.end();) {
auto segment = it->Segment().lock().get();
if (isCondition(segment) &&
segment->GetIndex() >= filterIndex) {
it = res.erase(it);
continue;
}
if (isAction(segment) || isElseAction(segment)) {
it = res.erase(it);
continue;
}
++it;
}
return res;
}
// Remove all else actions and actions after filterIndex
if (isAction(filter)) {
for (auto it = res.begin(); it != res.end();) {
auto segment = it->Segment().lock().get();
if (isAction(segment) &&
segment->GetIndex() >= filterIndex) {
it = res.erase(it);
continue;
}
if (isElseAction(segment)) {
it = res.erase(it);
continue;
}
++it;
}
return res;
}
// Remove all actions and elseActions after filterIndex
for (auto it = res.begin(); it != res.end();) {
auto segment = it->Segment().lock().get();
if (isElseAction(segment) &&
segment->GetIndex() >= filterIndex) {
it = res.erase(it);
continue;
}
if (isAction(segment)) {
it = res.erase(it);
continue;
}
++it;
}
return res;
}
std::optional<const TempVariable> Macro::GetTempVar(const MacroSegment *segment,
const std::string &id) const
{
if (!segment) {
return {};
}
return segment->GetTempVar(id);
}
void Macro::InvalidateTempVarValues() const
{
auto invalidateHelper =
[](const std::deque<std::shared_ptr<MacroSegment>> &segments) {
for (const auto &s : segments) {
s->InvalidateTempVarValues();
}
};
invalidateHelper({_conditions.begin(), _conditions.end()});
invalidateHelper({_actions.begin(), _actions.end()});
invalidateHelper({_elseActions.begin(), _elseActions.end()});
}
std::deque<std::shared_ptr<MacroCondition>> &Macro::Conditions()
{
return _conditions;
@@ -610,6 +735,9 @@ bool Macro::PostLoad()
for (auto &a : _actions) {
a->PostLoad();
}
for (auto &a : _elseActions) {
a->PostLoad();
}
return true;
}
@@ -1193,4 +1321,11 @@ std::weak_ptr<Macro> GetWeakMacroByName(const char *name)
return {};
}
void InvalidateMacroTempVarValues()
{
for (auto &m : switcher->macros) {
m->InvalidateTempVarValues();
}
}
} // namespace advss

View File

@@ -3,6 +3,7 @@
#include "macro-condition.hpp"
#include "macro-ref.hpp"
#include "variable-string.hpp"
#include "temp-variable.hpp"
#include <QString>
#include <QByteArray>
@@ -47,6 +48,13 @@ public:
bool GetStop() const { return _stop; }
void Stop();
// Temporary variable helpers
std::vector<TempVariable> GetTempVars(MacroSegment *filter) const;
std::optional<const TempVariable>
GetTempVar(const MacroSegment *, const std::string &id) const;
void InvalidateTempVarValues() const;
// Macro segments
std::deque<std::shared_ptr<MacroCondition>> &Conditions();
std::deque<std::shared_ptr<MacroAction>> &Actions();
std::deque<std::shared_ptr<MacroAction>> &ElseActions();
@@ -121,11 +129,13 @@ private:
void SetupHotkeys();
void ClearHotkeys() const;
void SetHotkeysDesc() const;
bool RunActionsHelper(
const std::deque<std::shared_ptr<MacroAction>> &actions,
bool ignorePause);
bool RunActions(bool ignorePause);
bool RunElseActions(bool ignorePause);
bool DockIsVisible() const;
void SetDockWidgetName() const;
void SaveDockSettings(obs_data_t *obj) const;
@@ -195,5 +205,6 @@ private:
Macro *GetMacroByName(const char *name);
Macro *GetMacroByQString(const QString &name);
std::weak_ptr<Macro> GetWeakMacroByName(const char *name);
void InvalidateMacroTempVarValues();
} // namespace advss

View File

@@ -19,8 +19,7 @@ bool MacroConditionMidi::CheckCondition()
for (const auto &m : *messages) {
if (m.Matches(_message)) {
SetVariableValue(std::to_string(m.Note()) + " " +
std::to_string(m.Value()));
SetVariableValues(m);
return true;
}
}
@@ -49,6 +48,36 @@ std::string MacroConditionMidi::GetShortDesc() const
return _device.Name();
}
void MacroConditionMidi::SetupTempVars()
{
MacroCondition::SetupTempVars();
AddTempvar("type",
obs_module_text("AdvSceneSwitcher.tempVar.midi.type"));
AddTempvar("channel",
obs_module_text("AdvSceneSwitcher.tempVar.midi.channel"));
AddTempvar("note",
obs_module_text("AdvSceneSwitcher.tempVar.midi.note"));
AddTempvar("value1",
obs_module_text("AdvSceneSwitcher.tempVar.midi.value1"));
AddTempvar("value2",
obs_module_text("AdvSceneSwitcher.tempVar.midi.value2"));
}
void MacroConditionMidi::SetVariableValues(const MidiMessage &m)
{
SetVariableValue(std::to_string(m.Note()) + " " +
std::to_string(m.Value()));
SetTempVarValue("type", m.MidiTypeToString(m.Type()));
SetTempVarValue("channel", std::to_string(m.Channel()));
try {
SetTempVarValue("note",
GetAllNotes().at(m.Note()).toStdString());
} catch (...) {
}
SetTempVarValue("value1", std::to_string(m.Note()));
SetTempVarValue("value2", std::to_string(m.Value()));
}
MacroConditionMidiEdit::MacroConditionMidiEdit(
QWidget *parent, std::shared_ptr<MacroConditionMidi> entryData)
: QWidget(parent),

View File

@@ -24,6 +24,9 @@ public:
MidiMessage _message;
private:
void SetupTempVars();
void SetVariableValues(const MidiMessage &);
static bool _registered;
static const std::string id;
};

View File

@@ -1,15 +1,17 @@
#include "midi-helpers.hpp"
#include <utility.hpp>
#include <log-helper.hpp>
#include <obs-module-helper.hpp>
#include <switcher-data.hpp>
#include <plugin-state-helper.hpp>
#include <sync-helper.hpp>
namespace advss {
static std::map<std::pair<MidiDeviceType, int>, MidiDeviceInstance *>
SetupMidiMessageVector()
{
GetSwitcher()->AddIntervalResetStep(
AddIntervalResetStep(
MidiDeviceInstance::ClearMessageBuffersOfAllDevices);
return {};
}
@@ -920,7 +922,6 @@ QStringList GetAllNotes()
"F#", "G", "G#", "A", "A#", "B"};
for (int octave = -1; octave <= 9; octave++) {
for (int noteIndex = 0; noteIndex < 12; noteIndex++) {
int midiNote = octave * 12 + noteIndex;
result << notes[noteIndex] + QString::number(octave);
}
}

View File

@@ -27,7 +27,8 @@ find_package(ZLIB)
# --- End of section ---
add_library(${PROJECT_NAME} MODULE)
target_compile_definitions(${PROJECT_NAME} PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT=1)
target_compile_definitions(${PROJECT_NAME} PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT=1
ASIO_HAS_OPENSSSL=1)
if(OS_MACOS)
target_compile_definitions(
${PROJECT_NAME} PRIVATE CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN=1)
@@ -39,8 +40,18 @@ target_sources(
${PROJECT_NAME}
PRIVATE category-selection.cpp
category-selection.hpp
channel-selection.cpp
channel-selection.hpp
chat-connection.cpp
chat-connection.hpp
event-sub.cpp
event-sub.hpp
macro-action-twitch.cpp
macro-action-twitch.hpp
macro-condition-twitch.cpp
macro-condition-twitch.hpp
points-reward-selection.cpp
points-reward-selection.hpp
token.cpp
token.hpp
twitch-helpers.cpp

View File

@@ -110,7 +110,7 @@ void TwitchCategorySelection::showPopup()
bool TwitchCategorySelection::IsPopulated()
{
return count() == _categoryGrabber.GetCategories().size() &&
return count() == (int)_categoryGrabber.GetCategories().size() &&
_fetchingCategoriesDone;
}
@@ -190,7 +190,7 @@ void CategoryGrabber::Search(const std::string &)
std::string cursor;
httplib::Params params = {
{"first", "100"}, {"after", cursor}, {"query", _searchString}};
auto response = SendGetRequest(uri, path, *_token, params);
auto response = SendGetRequest(*_token, uri, path, params);
while (response.status == 200 && !_stop) {
cursor = ParseReply(response.data);
@@ -200,7 +200,7 @@ void CategoryGrabber::Search(const std::string &)
params = {{"first", "100"},
{"after", cursor},
{"query", _searchString}};
response = SendGetRequest(uri, path, *_token, params);
response = SendGetRequest(*_token, uri, path, params);
emit CategoryCountUpdated(_categoryMap.size() - startCount);
}
}
@@ -214,7 +214,7 @@ void CategoryGrabber::GetAll()
static std::string cursor;
httplib::Params params = {{"first", "100"}, {"after", cursor}};
auto response = SendGetRequest(uri, path, *_token, params);
auto response = SendGetRequest(*_token, uri, path, params);
while (response.status == 200 && !_stop) {
cursor = ParseReply(response.data);
@@ -222,7 +222,7 @@ void CategoryGrabber::GetAll()
break; // End of category list
}
params = {{"first", "100"}, {"after", cursor}};
response = SendGetRequest(uri, path, *_token, params);
response = SendGetRequest(*_token, uri, path, params);
emit CategoryCountUpdated(_categoryMap.size());
}
}
@@ -249,7 +249,6 @@ ProgressDialog::ProgressDialog(QWidget *parent, bool showSkip)
"AdvSceneSwitcher.twitchCategories.fetchStart")))
{
setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
_skipFetchCheckBox->setVisible(showSkip);
auto layout = new QVBoxLayout(this);
layout->addWidget(_status);
auto cancelButton = new QPushButton(
@@ -264,6 +263,7 @@ ProgressDialog::ProgressDialog(QWidget *parent, bool showSkip)
QWidget::connect(cancelButton, &QPushButton::clicked, this,
[this]() { _skipFetch ? accept() : reject(); });
_skipFetchCheckBox->setVisible(showSkip);
if (_skipFetch) {
accept();
}
@@ -277,7 +277,8 @@ void ProgressDialog::CategoryCountUpdated(int value)
.arg(value));
}
TwitchCategorySearchButton::TwitchCategorySearchButton()
TwitchCategorySearchButton::TwitchCategorySearchButton(QWidget *parent)
: QPushButton(parent)
{
setMaximumWidth(22);
const std::string pathPrefix =
@@ -355,6 +356,33 @@ void TwitchCategorySearchButton::StartManualCategorySearch()
}
}
TwitchCategoryWidget::TwitchCategoryWidget(QWidget *parent)
: QWidget(parent),
_selection(new TwitchCategorySelection(this)),
_manualSearch(new TwitchCategorySearchButton(this))
{
QWidget::connect(_selection,
SIGNAL(CategoreyChanged(const TwitchCategory &)), this,
SIGNAL(CategoreyChanged(const TwitchCategory &)));
auto layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_selection);
layout->addWidget(_manualSearch);
setLayout(layout);
}
void TwitchCategoryWidget::SetCategory(const TwitchCategory &category)
{
_selection->SetCategory(category);
}
void TwitchCategoryWidget::SetToken(const std::weak_ptr<TwitchToken> &token)
{
_selection->SetToken(token);
_manualSearch->SetToken(token);
}
TwitchCategorySignalManager *TwitchCategorySignalManager::Instance()
{
static TwitchCategorySignalManager manager;

View File

@@ -69,12 +69,7 @@ class TwitchCategorySelection : public FilterComboBox {
public:
TwitchCategorySelection(QWidget *parent);
// Will *not* verify if ID is still valid or populate the selection
// list as that would take too long
void SetCategory(const TwitchCategory &);
// Used for populating the category list
void SetToken(const std::weak_ptr<TwitchToken> &);
private slots:
@@ -103,7 +98,7 @@ class TwitchCategorySearchButton : public QPushButton {
Q_OBJECT
public:
TwitchCategorySearchButton();
TwitchCategorySearchButton(QWidget *parent);
void SetToken(const std::weak_ptr<TwitchToken> &);
private slots:
@@ -116,6 +111,27 @@ private:
std::weak_ptr<TwitchToken> _token;
};
class TwitchCategoryWidget : public QWidget {
Q_OBJECT
public:
TwitchCategoryWidget(QWidget *parent);
// Will *not* verify if ID is still valid or populate the selection
// list as that would take too long
void SetCategory(const TwitchCategory &);
// Used for populating the category list
void SetToken(const std::weak_ptr<TwitchToken> &);
signals:
void CategoreyChanged(const TwitchCategory &);
private:
TwitchCategorySelection *_selection;
TwitchCategorySearchButton *_manualSearch;
};
// Helper class to ease singal / slot handling
class TwitchCategorySignalManager : public QObject {
Q_OBJECT

View File

@@ -0,0 +1,263 @@
#include "channel-selection.hpp"
#include "twitch-helpers.hpp"
#include "token.hpp"
#include <QLayout>
#include <QDesktopServices>
#include <obs-module-helper.hpp>
#include <nlohmann/json.hpp>
namespace advss {
bool ChannelLiveInfo::IsLive() const
{
return type == "live";
}
void TwitchChannel::Load(obs_data_t *obj)
{
OBSDataAutoRelease data = obs_data_get_obj(obj, "channel");
_name.Load(data, "name");
}
void TwitchChannel::Save(obs_data_t *obj) const
{
OBSDataAutoRelease data = obs_data_create();
_name.Save(data, "name");
obs_data_set_obj(obj, "channel", data);
}
std::string TwitchChannel::GetUserID(const TwitchToken &token) const
{
static std::map<std::string, std::string> userIDCache;
auto it = userIDCache.find(std::string(_name));
if (it != userIDCache.end()) {
return it->second;
}
auto res = SendGetRequest(token, "https://api.twitch.tv",
"/helix/users", {{"login", _name}});
if (res.status == 400) {
userIDCache[_name] = "invalid";
blog(LOG_INFO, "ignoring invalid channel selection '%s'!",
_name.c_str());
return "invalid";
}
if (res.status != 200) {
blog(LOG_INFO, "failed to get Twitch user id from token! (%d)",
res.status);
return "invalid";
}
OBSDataArrayAutoRelease array = obs_data_get_array(res.data, "data");
size_t count = obs_data_array_count(array);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayObj = obs_data_array_item(array, i);
std::string id = obs_data_get_string(arrayObj, "id");
userIDCache[_name] = id;
return id;
}
userIDCache[_name] = "invalid";
return "invalid";
}
static QDate parseRFC3339Date(const std::string &rfc3339Date)
{
QString rfc3339QString = QString::fromStdString(rfc3339Date);
QRegularExpression regex("(\\d{4})-(\\d{2})-(\\d{2})");
QRegularExpressionMatch match = regex.match(rfc3339QString);
if (match.hasMatch()) {
int year = match.captured(1).toInt();
int month = match.captured(2).toInt();
int day = match.captured(3).toInt();
QDate date(year, month, day);
return date;
} else {
return QDate();
}
}
static std::vector<std::string>
getStringArrayHelper(const OBSDataAutoRelease &data, const std::string &value)
{
std::vector<std::string> result;
auto jsonStr = obs_data_get_json(data);
if (!jsonStr) {
return result;
}
try {
auto json = nlohmann::json::parse(jsonStr);
auto array = json[value];
if (!array.is_array()) {
return result;
}
for (const auto &element : array) {
std::string strElement = element.get<std::string>();
result.emplace_back(strElement);
}
} catch (const nlohmann::json::exception &) {
}
return result;
}
std::optional<ChannelLiveInfo>
TwitchChannel::GetLiveInfo(const TwitchToken &token)
{
auto id = GetUserID(token);
if (!IsValid(id)) {
return {};
}
httplib::Params params = {
{"first", "1"}, {"after", ""}, {"user_id", id}};
auto result = SendGetRequest(token, "https://api.twitch.tv",
"/helix/streams", params, true);
if (result.status != 200) {
return {};
}
OBSDataArrayAutoRelease array = obs_data_get_array(result.data, "data");
size_t count = obs_data_array_count(array);
if (count == 0) {
return {};
}
OBSDataAutoRelease data = obs_data_array_item(array, 0);
ChannelLiveInfo info;
info.id = obs_data_get_string(data, "id");
info.user_id = obs_data_get_string(data, "user_id");
info.user_login = obs_data_get_string(data, "user_login");
info.user_name = obs_data_get_string(data, "user_name");
info.game_id = obs_data_get_string(data, "game_id");
info.game_name = obs_data_get_string(data, "game_name");
info.type = obs_data_get_string(data, "type");
info.title = obs_data_get_string(data, "title");
info.tags = getStringArrayHelper(data, "tags");
info.viewer_count = obs_data_get_int(data, "viewer_count");
info.started_at =
parseRFC3339Date(obs_data_get_string(data, "started_at"));
info.language = obs_data_get_string(data, "language");
info.thumbnail_url = obs_data_get_string(data, "thumbnail_url");
info.is_mature = obs_data_get_bool(data, "is_mature");
return info;
}
std::optional<ChannelInfo> TwitchChannel::GetInfo(const TwitchToken &token)
{
auto id = GetUserID(token);
if (!IsValid(id)) {
return {};
}
httplib::Params params = {
{"first", "1"}, {"after", ""}, {"broadcaster_id", id}};
auto result = SendGetRequest(token, "https://api.twitch.tv",
"/helix/channels", params, true);
if (result.status != 200) {
return {};
}
OBSDataArrayAutoRelease array = obs_data_get_array(result.data, "data");
size_t count = obs_data_array_count(array);
if (count == 0) {
return {};
}
OBSDataAutoRelease data = obs_data_array_item(array, 0);
ChannelInfo info;
info.broadcaster_id = obs_data_get_string(data, "broadcaster_id");
info.broadcaster_login = obs_data_get_string(data, "broadcaster_login");
info.broadcaster_name = obs_data_get_string(data, "broadcaster_name");
info.broadcaster_language =
obs_data_get_string(data, "broadcaster_language");
info.game_name = obs_data_get_string(data, "game_name");
info.game_id = obs_data_get_string(data, "game_id");
info.title = obs_data_get_string(data, "title");
info.delay = obs_data_get_int(data, "delay");
info.tags = getStringArrayHelper(data, "tags");
info.content_classification_labels =
getStringArrayHelper(data, "content_classification_labels");
info.is_branded_content = obs_data_get_bool(data, "is_branded_content");
return info;
}
bool TwitchChannel::IsValid(const std::string &id) const
{
return id != "invalid" && !id.empty();
}
TwitchChannelSelection::TwitchChannelSelection(QWidget *parent)
: QWidget(parent),
_channelName(new VariableLineEdit(this)),
_openChannel(new QPushButton(obs_module_text(
"AdvSceneSwitcher.twitch.selection.channel.open")))
{
QWidget::connect(_channelName, SIGNAL(editingFinished()), this,
SLOT(SelectionChanged()));
QWidget::connect(_openChannel, SIGNAL(pressed()), this,
SLOT(OpenChannel()));
QWidget::connect(this, SIGNAL(ChannelChanged(const TwitchChannel &)),
this,
SLOT(SetOpenChannelState(const TwitchChannel &)));
auto layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_channelName);
layout->addWidget(_openChannel);
setLayout(layout);
}
void TwitchChannelSelection::SetChannel(const TwitchChannel &channel)
{
_channelName->setText(channel._name);
emit ChannelChanged(channel);
}
void TwitchChannelSelection::SetToken(const std::weak_ptr<TwitchToken> &token)
{
_token = token;
}
void TwitchChannelSelection::OpenChannel()
{
// Resolve potential variables
StringVariable temp = _channelName->text().toStdString();
QDesktopServices::openUrl("https://www.twitch.tv/" +
QString::fromStdString(temp));
}
void TwitchChannelSelection::SetOpenChannelState(const TwitchChannel &channel)
{
auto token = _token.lock();
if (!token) {
_openChannel->setToolTip(obs_module_text(
"AdvSceneSwitcher.twitch.selection.channel.open.tooltip.noAccount"));
_openChannel->setDisabled(false);
return;
}
if (channel.GetUserID(*token) != "invalid") {
_openChannel->setToolTip(obs_module_text(
"AdvSceneSwitcher.twitch.selection.channel.open.tooltip.details"));
_openChannel->setDisabled(false);
return;
}
_openChannel->setToolTip(obs_module_text(
"AdvSceneSwitcher.twitch.selection.channel.open.tooltip.noChannel"));
_openChannel->setDisabled(true);
}
void TwitchChannelSelection::SelectionChanged()
{
TwitchChannel channel;
channel._name = _channelName->text().toStdString();
emit ChannelChanged(channel);
}
} // namespace advss

View File

@@ -0,0 +1,86 @@
#pragma once
#include <obs-data.h>
#include <variable-line-edit.hpp>
#include <QPushButton>
#include <QDate>
namespace advss {
class TwitchToken;
// Information returned by the https://api.twitch.tv/helix/streams API call
// Channel must be live
struct ChannelLiveInfo {
bool IsLive() const;
std::string id;
std::string user_id;
std::string user_login;
std::string user_name;
std::string game_id;
std::string game_name;
std::string type;
std::string title;
std::vector<std::string> tags;
int viewer_count;
QDate started_at;
std::string language;
std::string thumbnail_url;
bool is_mature;
};
// Information returned by the https://api.twitch.tv/helix/channels API call
// Channel can be offline
struct ChannelInfo {
std::string broadcaster_id;
std::string broadcaster_login;
std::string broadcaster_name;
std::string broadcaster_language;
std::string game_name;
std::string game_id;
std::string title;
uint32_t delay;
std::vector<std::string> tags;
std::vector<std::string> content_classification_labels;
bool is_branded_content;
};
struct TwitchChannel {
void Load(obs_data_t *obj);
void Save(obs_data_t *obj) const;
StringVariable GetName() const { return _name; };
std::string GetUserID(const TwitchToken &token) const;
bool IsValid(const std::string &id) const;
std::optional<ChannelLiveInfo> GetLiveInfo(const TwitchToken &);
std::optional<ChannelInfo> GetInfo(const TwitchToken &);
private:
StringVariable _name = "";
friend class TwitchChannelSelection;
};
class TwitchChannelSelection : public QWidget {
Q_OBJECT
public:
TwitchChannelSelection(QWidget *parent);
void SetChannel(const TwitchChannel &channel);
void SetToken(const std::weak_ptr<TwitchToken> &token);
private slots:
void SelectionChanged();
void OpenChannel();
void SetOpenChannelState(const TwitchChannel &channel);
signals:
void ChannelChanged(const TwitchChannel &);
private:
VariableLineEdit *_channelName;
QPushButton *_openChannel;
std::weak_ptr<TwitchToken> _token;
};
} // namespace advss

View File

@@ -0,0 +1,719 @@
#include "chat-connection.hpp"
#include "token.hpp"
#include "twitch-helpers.hpp"
#include <log-helper.hpp>
#include <plugin-state-helper.hpp>
namespace advss {
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;
static constexpr int reconnectDelay = 15;
static constexpr int messageIDBufferSize = 20;
/* ------------------------------------------------------------------------- */
static ParsedTags parseTags(const std::string &tags)
{
ParsedTags parsedTags;
static constexpr std::array<std::string_view, 2> tagsToIgnore = {
"client-nonce", "flags"};
std::vector<std::string> parsedTagPairs;
size_t pos = 0;
size_t endPos;
while ((endPos = tags.find(';', pos)) != std::string::npos) {
parsedTagPairs.push_back(tags.substr(pos, endPos - pos));
pos = endPos + 1;
}
parsedTagPairs.push_back(tags.substr(pos));
for (const auto &tagPair : parsedTagPairs) {
size_t equalsPos = tagPair.find('=');
std::string tagName = tagPair.substr(0, equalsPos);
std::string tagValue = (equalsPos != std::string::npos)
? tagPair.substr(equalsPos + 1)
: "";
if (std::find(tagsToIgnore.begin(), tagsToIgnore.end(),
tagName) != tagsToIgnore.end()) {
continue;
}
if (tagValue.empty()) {
parsedTags.tagMap[tagName] = {};
continue;
}
if (tagName == "badges" || tagName == "badge-info") {
ParsedTags::BadgeMap badgeMap;
std::vector<std::string> badgePairs;
size_t badgePos = 0;
size_t badgeEndPos;
while ((badgeEndPos = tagValue.find(',', badgePos)) !=
std::string::npos) {
badgePairs.push_back(tagValue.substr(
badgePos, badgeEndPos - badgePos));
badgePos = badgeEndPos + 1;
}
badgePairs.push_back(tagValue.substr(badgePos));
for (const auto &badgePair : badgePairs) {
size_t slashPos = badgePair.find('/');
std::string badgeName =
badgePair.substr(0, slashPos);
std::string badgeValue =
(slashPos != std::string::npos)
? badgePair.substr(slashPos + 1)
: "";
badgeMap[badgeName] = badgeValue;
}
parsedTags.tagMap[tagName] = badgeMap;
} else if (tagName == "emotes") {
ParsedTags::EmoteMap emoteMap;
std::vector<std::string> emotePairs;
size_t emotePos = 0;
size_t emoteEndPos;
while ((emoteEndPos = tagValue.find('/', emotePos)) !=
std::string::npos) {
emotePairs.push_back(tagValue.substr(
emotePos, emoteEndPos - emotePos));
emotePos = emoteEndPos + 1;
}
emotePairs.push_back(tagValue.substr(emotePos));
for (const auto &emotePair : emotePairs) {
size_t colonPos = emotePair.find(':');
std::string emoteId =
emotePair.substr(0, colonPos);
std::string positions =
(colonPos != std::string::npos)
? emotePair.substr(colonPos + 1)
: "";
std::vector<std::pair<int, int>> textPositions;
size_t positionPos = 0;
size_t positionEndPos;
while ((positionEndPos = positions.find(
',', positionPos)) !=
std::string::npos) {
std::string position = positions.substr(
positionPos,
positionEndPos - positionPos);
size_t dashPos = position.find('-');
int startPos = std::stoi(
position.substr(0, dashPos));
int endPos = std::stoi(
position.substr(dashPos + 1));
textPositions.push_back(
{startPos, endPos});
positionPos = positionEndPos + 1;
}
textPositions.push_back(
{std::stoi(
positions.substr(positionPos)),
std::stoi(positions.substr(
positionPos))});
emoteMap[emoteId] = textPositions;
}
parsedTags.tagMap[tagName] = emoteMap;
} else if (tagName == "emote-sets") {
ParsedTags::EmoteSet emoteSetIds;
size_t setIdPos = 0;
size_t setIdEndPos;
while ((setIdEndPos = tagValue.find(',', setIdPos)) !=
std::string::npos) {
emoteSetIds.push_back(tagValue.substr(
setIdPos, setIdEndPos - setIdPos));
setIdPos = setIdEndPos + 1;
}
emoteSetIds.push_back(tagValue.substr(setIdPos));
parsedTags.tagMap[tagName] = emoteSetIds;
} else {
parsedTags.tagMap[tagName] = tagValue;
}
}
return parsedTags;
}
static void parseSource(const std::string &rawSourceComponent,
IRCMessage &message)
{
if (rawSourceComponent.empty()) {
return;
}
size_t nickPos = 0;
size_t nickEndPos = rawSourceComponent.find('!');
if (nickEndPos != std::string::npos) {
message.source.nick = rawSourceComponent.substr(
nickPos, nickEndPos - nickPos);
message.source.host = rawSourceComponent.substr(nickEndPos + 1);
} else {
// Assume the entire source is the host if no '!' is found
message.source.host = rawSourceComponent;
}
}
static void parseCommand(const std::string &rawCommandComponent,
IRCMessage &message)
{
std::vector<std::string> commandParts;
size_t pos = 0;
size_t endPos;
while ((endPos = rawCommandComponent.find(' ', pos)) !=
std::string::npos) {
commandParts.push_back(
rawCommandComponent.substr(pos, endPos - pos));
pos = endPos + 1;
}
commandParts.push_back(rawCommandComponent.substr(pos));
if (commandParts.empty()) {
return;
}
message.command.command = commandParts[0];
if (message.command.command == "CAP") {
if (commandParts.size() < 3) {
return;
}
message.command.parameters = (commandParts[2] == "ACK");
} else if (message.command.command == "RECONNECT") {
blog(LOG_INFO,
"The Twitch IRC server is about to terminate the connection for maintenance.");
} else if (message.command.command == "421") {
// Unsupported command
return;
} else if (message.command.command == "PING" ||
message.command.command == "001" ||
message.command.command == "JOIN" ||
message.command.command == "PART" ||
message.command.command == "NOTICE" ||
message.command.command == "CLEARCHAT" ||
message.command.command == "HOSTTARGET" ||
message.command.command == "PRIVMSG") {
if (commandParts.size() < 2) {
return;
}
message.command.parameters = commandParts[1];
} else if (message.command.command == "GLOBALUSERSTATE" ||
message.command.command == "USERSTATE" ||
message.command.command == "ROOMSTATE" ||
message.command.command == "002" ||
message.command.command == "003" ||
message.command.command == "004" ||
message.command.command == "353" ||
message.command.command == "366" ||
message.command.command == "372" ||
message.command.command == "375" ||
message.command.command == "376") {
// Do nothing for these cases for now
} else {
vblog(LOG_INFO, "Unexpected IRC command: %s",
message.command.command.c_str());
}
}
static IRCMessage parseMessage(const std::string &message)
{
IRCMessage parsedMessage;
size_t idx = 0;
std::string rawTagsComponent;
std::string rawSourceComponent;
std::string rawCommandComponent;
std::string rawMessageComponent;
if (message[idx] == '@') {
size_t endIdx = message.find(' ');
rawTagsComponent = message.substr(1, endIdx - 1);
idx = endIdx + 1;
}
if (message[idx] == ':') {
idx += 1;
size_t endIdx = message.find(' ', idx);
rawSourceComponent = message.substr(idx, endIdx - idx);
idx = endIdx + 1;
}
size_t endIdx = message.find(':', idx);
if (endIdx == std::string::npos) {
endIdx = message.length();
}
rawCommandComponent = message.substr(idx, endIdx - idx);
if (endIdx != message.length()) {
idx = endIdx + 1;
rawMessageComponent = message.substr(idx);
}
parsedMessage.tags = parseTags(rawTagsComponent);
parseSource(rawSourceComponent, parsedMessage);
parseCommand(rawCommandComponent, parsedMessage);
parsedMessage.message = rawMessageComponent;
return parsedMessage;
}
static std::vector<IRCMessage> parseMessages(const std::string &messagesStr)
{
static constexpr std::string_view delimiter = "\r\n";
std::vector<IRCMessage> messages;
auto isWhitespace = [](const std::string &str) -> bool {
for (char c : str) {
if (!std::isspace(static_cast<unsigned char>(c))) {
return false;
}
}
return true;
};
size_t start = 0;
size_t end = messagesStr.find(delimiter);
while (end != std::string::npos) {
auto message = messagesStr.substr(start, end - start);
if (isWhitespace(message)) {
continue;
}
auto parsedMessage = parseMessage(message);
if (parsedMessage.command.command.empty()) {
vblog(LOG_INFO, "discarding IRC message: %s",
message.c_str());
} else {
messages.emplace_back(parsedMessage);
}
start = end + delimiter.length();
end = messagesStr.find(delimiter, start);
}
auto message = messagesStr.substr(start);
if (isWhitespace(message)) {
return messages;
}
auto parsedMessage = parseMessage(message);
if (parsedMessage.command.command.empty()) {
vblog(LOG_INFO, "discarding IRC message: %s", message.c_str());
return messages;
}
messages.emplace_back(parsedMessage);
return messages;
}
/* ------------------------------------------------------------------------- */
static constexpr std::string_view defaultURL =
"wss://irc-ws.chat.twitch.tv:443";
std::map<TwitchChatConnection::ChatMapKey, std::weak_ptr<TwitchChatConnection>>
TwitchChatConnection::_chatMap = {};
TwitchChatConnection::TwitchChatConnection(const TwitchToken &token,
const TwitchChannel &channel)
: QObject(nullptr), _token(token), _channel(channel)
{
_client.get_alog().clear_channels(
websocketpp::log::alevel::frame_header |
websocketpp::log::alevel::frame_payload |
websocketpp::log::alevel::control);
_client.init_asio();
#ifndef _WIN32
_client.set_reuse_addr(true);
#endif
_client.set_open_handler(bind(&TwitchChatConnection::OnOpen, this, _1));
_client.set_message_handler(
bind(&TwitchChatConnection::OnMessage, this, _1, _2));
_client.set_close_handler(
bind(&TwitchChatConnection::OnClose, this, _1));
_client.set_fail_handler(bind(&TwitchChatConnection::OnFail, this, _1));
_client.set_tls_init_handler([](websocketpp::connection_hdl) {
return websocketpp::lib::make_shared<asio::ssl::context>(
asio::ssl::context::sslv23_client);
});
_url = defaultURL.data();
RegisterInstance();
}
TwitchChatConnection::~TwitchChatConnection()
{
Disconnect();
UnregisterInstance();
}
static bool setupChatMessageClear()
{
AddIntervalResetStep(&TwitchChatConnection::ClearAllMessages);
return true;
}
bool TwitchChatConnection::_setupDone = setupChatMessageClear();
void TwitchChatConnection::ClearMessages()
{
std::lock_guard<std::mutex> lock(_messageMtx);
_messages.clear();
}
std::mutex TwitchChatConnection::_instancesMtx;
std::vector<TwitchChatConnection *> TwitchChatConnection::_instances;
void TwitchChatConnection::RegisterInstance()
{
std::lock_guard<std::mutex> lock(_instancesMtx);
_instances.emplace_back(this);
}
void TwitchChatConnection::UnregisterInstance()
{
std::lock_guard<std::mutex> lock(_instancesMtx);
auto it = std::remove(_instances.begin(), _instances.end(), this);
_instances.erase(it, _instances.end());
}
std::shared_ptr<TwitchChatConnection>
TwitchChatConnection::GetChatConnection(const TwitchToken &token_,
const TwitchChannel &channel)
{
auto token = token_.GetToken();
if (!token ||
!token_.AnyOptionIsEnabled({{"chat:read"}, {"chat:edit"}}) ||
channel.GetName().empty()) {
return {};
}
auto key = ChatMapKey{channel.GetName(), *token};
auto it = _chatMap.find(key);
if (it != _chatMap.end()) {
auto connection = it->second.lock();
if (connection) {
return connection;
}
}
auto connection = std::shared_ptr<TwitchChatConnection>(
new TwitchChatConnection(token_, channel));
_chatMap[key] = connection;
return connection;
}
void TwitchChatConnection::ClearAllMessages()
{
std::lock_guard<std::mutex> lock(_instancesMtx);
for (const auto &TwitchChatConnection : _instances) {
TwitchChatConnection->ClearMessages();
}
}
void TwitchChatConnection::ConnectThread()
{
while (!_disconnect) {
std::unique_lock<std::mutex> lock(_waitMtx);
_client.reset();
_connected = true;
websocketpp::lib::error_code ec;
websocketpp::client<websocketpp::config::asio_tls_client>::
connection_ptr con = _client.get_connection(_url, ec);
if (ec) {
blog(LOG_INFO, "Twitch TwitchChatConnection failed: %s",
ec.message().c_str());
} else {
_client.connect(con);
_connection = connection_hdl(con);
_client.run();
}
blog(LOG_INFO,
"Twitch TwitchChatConnection trying to reconnect to in %d seconds.",
reconnectDelay);
_cv.wait_for(lock, std::chrono::seconds(reconnectDelay));
}
_connected = false;
}
void TwitchChatConnection::Connect()
{
std::lock_guard<std::mutex> lock(_connectMtx);
if (_connected) {
vblog(LOG_INFO,
"Twitch TwitchChatConnection connect already in progress");
return;
}
_disconnect = true;
if (_thread.joinable()) {
_thread.join();
}
_disconnect = false;
_thread = std::thread(&TwitchChatConnection::ConnectThread, this);
}
void TwitchChatConnection::Disconnect()
{
std::lock_guard<std::mutex> lock(_connectMtx);
_disconnect = true;
websocketpp::lib::error_code ec;
_client.close(_connection, websocketpp::close::status::normal,
"Twitch chat connection stopping", ec);
{
std::unique_lock<std::mutex> waitLock(_waitMtx);
_cv.notify_all();
}
while (_connected) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
_client.close(_connection, websocketpp::close::status::normal,
"Twitch chat connection stopping", ec);
}
if (_thread.joinable()) {
_thread.join();
}
_connected = false;
}
static std::string toLowerCase(const std::string &str)
{
std::string result;
result.reserve(str.length());
for (char c : str) {
result.push_back(std::tolower(static_cast<unsigned char>(c)));
}
return result;
}
std::vector<IRCMessage> TwitchChatConnection::Messages()
{
if (!_connected) {
Connect();
return {};
}
if (_joinedChannelName !=
"#" + toLowerCase(std::string(_channel.GetName()))) {
// TODO: disconnect previous channel?
JoinChannel(_channel.GetName());
return {};
}
std::lock_guard<std::mutex> lock(_messageMtx);
return _messages;
}
std::vector<IRCMessage> TwitchChatConnection::Whispers()
{
if (!_connected) {
Connect();
return {};
}
if (_joinedChannelName !=
"#" + toLowerCase(std::string(_channel.GetName()))) {
// TODO: disconnect previous channel?
JoinChannel(_channel.GetName());
return {};
}
std::lock_guard<std::mutex> lock(_messageMtx);
return _whispers;
}
void TwitchChatConnection::SendChatMessage(const std::string &message)
{
if (!_connected) {
Connect();
return;
}
if (_joinedChannelName !=
"#" + toLowerCase(std::string(_channel.GetName()))) {
// TODO: disconnect previous channel?
JoinChannel(_channel.GetName());
return;
}
Send("PRIVMSG " + _joinedChannelName + " :" + message);
}
void TwitchChatConnection::Authenticate()
{
if (!_token.GetToken()) {
blog(LOG_INFO,
"Joining Twitch chat failed due to missing token!");
}
Send("CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands");
auto pass = _token.GetToken();
if (!pass.has_value()) {
blog(LOG_INFO,
"Joining Twitch chat failed due to invalid token!");
}
Send("PASS oauth:" + *pass);
Send("NICK " + _token.GetName());
}
void TwitchChatConnection::JoinChannel(const std::string &channelName)
{
if (!_authenticated) {
return;
}
Send("JOIN #" + channelName);
}
void TwitchChatConnection::HandleJoin(const IRCMessage &message)
{
std::lock_guard<std::mutex> lock(_messageMtx);
_joinedChannelName = std::get<std::string>(message.command.parameters);
vblog(LOG_INFO, "Twitch chat join was successful!");
}
void TwitchChatConnection::HandleNewMessage(const IRCMessage &message)
{
std::lock_guard<std::mutex> lock(_messageMtx);
_messages.emplace_back(message);
vblog(LOG_INFO, "Received new chat message %s",
message.message.c_str());
}
void TwitchChatConnection::HandleWhisper(const IRCMessage &message)
{
std::lock_guard<std::mutex> lock(_messageMtx);
_whispers.emplace_back(message);
vblog(LOG_INFO, "Received new chat whisper message %s",
message.message.c_str());
}
void TwitchChatConnection::HandleNotice(const IRCMessage &message) const
{
if (message.message == "Login unsuccessful") {
blog(LOG_INFO, "Twitch chat connection was unsuccessful: %s",
message.message.c_str());
return;
} else if (message.message ==
"You don't have permission to perform that action") {
blog(LOG_INFO,
"No permission. Check if the access token is still valid");
return;
}
vblog(LOG_INFO, "Twitch chat notice: %s", message.message.c_str());
}
void TwitchChatConnection::HandleReconnect()
{
blog(LOG_INFO,
"Received RECONNECT notice! Twitch chat connection will be terminated!");
Disconnect();
}
void TwitchChatConnection::OnOpen(connection_hdl)
{
vblog(LOG_INFO, "Twitch chat connection opened");
_connected = true;
Authenticate();
}
void TwitchChatConnection::OnMessage(
connection_hdl,
websocketpp::client<websocketpp::config::asio_tls_client>::message_ptr
message)
{
constexpr std::string_view authOKCommand = "001";
constexpr std::string_view pingCommand = "PING";
constexpr std::string_view joinOKCommand = "JOIN";
constexpr std::string_view noticeCommand = "NOTICE";
constexpr std::string_view reconnectCommand = "RECONNECT";
constexpr std::string_view newMessageCommand = "PRIVMSG";
constexpr std::string_view whisperCommand = "WHISPER";
if (!message) {
return;
}
if (message->get_opcode() != websocketpp::frame::opcode::text) {
return;
}
std::string payload = message->get_payload();
auto messages = parseMessages(payload);
for (const auto &message : messages) {
if (message.command.command == authOKCommand) {
vblog(LOG_INFO,
"Twitch chat connection authenticated!");
_authenticated = true;
} else if (message.command.command == pingCommand) {
Send("PONG " +
std::get<std::string>(message.command.parameters));
} else if (message.command.command == joinOKCommand) {
HandleJoin(message);
} else if (message.command.command == newMessageCommand) {
HandleNewMessage(message);
} else if (message.command.command == whisperCommand) {
HandleWhisper(message);
} else if (message.command.command == noticeCommand) {
HandleNotice(message);
} else if (message.command.command == reconnectCommand) {
HandleReconnect();
}
}
}
void TwitchChatConnection::OnClose(connection_hdl hdl)
{
websocketpp::client<websocketpp::config::asio_tls_client>::connection_ptr
con = _client.get_con_from_hdl(hdl);
auto msg = con->get_ec().message();
blog(LOG_INFO, "Twitch chat connection closed: %s", msg.c_str());
_connected = false;
}
void TwitchChatConnection::OnFail(connection_hdl hdl)
{
websocketpp::client<websocketpp::config::asio_tls_client>::connection_ptr
con = _client.get_con_from_hdl(hdl);
auto msg = con->get_ec().message();
blog(LOG_INFO, "Twitch chat connection failed: %s", msg.c_str());
_connected = false;
}
void TwitchChatConnection::Send(const std::string &msg)
{
if (_connection.expired()) {
return;
}
websocketpp::lib::error_code errorCode;
_client.send(_connection, msg, websocketpp::frame::opcode::text,
errorCode);
if (errorCode) {
std::string errorCodeMessage = errorCode.message();
blog(LOG_INFO, "Twitch chat websocket send failed: %s",
errorCodeMessage.c_str());
}
}
bool TwitchChatConnection::ChatMapKey::operator<(const ChatMapKey &other) const
{
return (channelName + token) < (other.channelName + other.token);
}
} // namespace advss

View File

@@ -0,0 +1,114 @@
#pragma once
#include "channel-selection.hpp"
#include "token.hpp"
#include <websocketpp/client.hpp>
#include <QObject>
#include <mutex>
#include <condition_variable>
#include <websocketpp/config/asio_client.hpp>
#include <websocketpp/config/asio.hpp>
namespace advss {
using websocketpp::connection_hdl;
struct ParsedTags {
using BadgeMap = std::unordered_map<std::string, std::string>;
using EmoteMap = std::unordered_map<std::string,
std::vector<std::pair<int, int>>>;
using EmoteSet = std::vector<std::string>;
std::unordered_map<std::string, std::variant<std::string, BadgeMap,
EmoteMap, EmoteSet>>
tagMap;
};
struct IRCMessage {
ParsedTags tags;
struct {
std::string nick;
std::string host;
} source;
struct {
std::string command;
std::variant<std::string, bool, std::vector<std::string>>
parameters;
} command;
std::string message;
};
class TwitchChatConnection : public QObject {
public:
~TwitchChatConnection();
static std::shared_ptr<TwitchChatConnection>
GetChatConnection(const TwitchToken &token,
const TwitchChannel &channel);
std::vector<IRCMessage> Messages();
std::vector<IRCMessage> Whispers();
void SendChatMessage(const std::string &message);
static void ClearAllMessages();
void ClearMessages();
private:
TwitchChatConnection(const TwitchToken &token,
const TwitchChannel &channel);
void Connect();
void Disconnect();
void OnOpen(connection_hdl hdl);
void
OnMessage(connection_hdl hdl,
websocketpp::client<websocketpp::config::asio_tls_client>::
message_ptr message);
void OnClose(connection_hdl hdl);
void OnFail(connection_hdl hdl);
void Send(const std::string &msg);
void ConnectThread();
void Authenticate();
void JoinChannel(const std::string &);
void HandleJoin(const IRCMessage &);
void HandleNewMessage(const IRCMessage &);
void HandleWhisper(const IRCMessage &);
void HandleNotice(const IRCMessage &) const;
void HandleReconnect();
void RegisterInstance();
void UnregisterInstance();
struct ChatMapKey {
std::string channelName;
std::string token;
bool operator<(const ChatMapKey &) const;
};
static std::map<ChatMapKey, std::weak_ptr<TwitchChatConnection>>
_chatMap;
TwitchToken _token;
TwitchChannel _channel;
std::string _joinedChannelName;
websocketpp::client<websocketpp::config::asio_tls_client> _client;
connection_hdl _connection;
std::thread _thread;
std::mutex _waitMtx;
std::mutex _connectMtx;
std::condition_variable _cv;
std::atomic_bool _connected{false};
std::atomic_bool _authenticated{false};
std::atomic_bool _disconnect{false};
std::string _url;
std::mutex _messageMtx;
std::vector<IRCMessage> _messages;
std::vector<IRCMessage> _whispers;
static std::mutex _instancesMtx;
static std::vector<TwitchChatConnection *> _instances;
static bool _setupDone;
};
} // namespace advss

View File

@@ -0,0 +1,441 @@
#include "event-sub.hpp"
#include "token.hpp"
#include "twitch-helpers.hpp"
#include <log-helper.hpp>
#include <plugin-state-helper.hpp>
namespace advss {
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;
#ifdef USE_TWITCH_CLI_MOCK
constexpr std::string_view defaultURL = "ws://127.0.0.1:8080/ws";
constexpr std::string_view registerSubscriptionURL = "http://127.0.0.1:8080";
constexpr std::string_view registerSubscriptionPath = "/eventsub/subscriptions";
#else
constexpr std::string_view defaultURL = "wss://eventsub.wss.twitch.tv/ws";
constexpr std::string_view registerSubscriptionURL = "https://api.twitch.tv";
constexpr std::string_view registerSubscriptionPath =
"/helix/eventsub/subscriptions";
#endif
const int reconnectDelay = 15;
const int messageIDBufferSize = 20;
EventSub::EventSub() : QObject(nullptr)
{
_client.get_alog().clear_channels(
websocketpp::log::alevel::frame_header |
websocketpp::log::alevel::frame_payload |
websocketpp::log::alevel::control);
_client.init_asio();
#ifndef _WIN32
_client.set_reuse_addr(true);
#endif
_client.set_open_handler(bind(&EventSub::OnOpen, this, _1));
_client.set_message_handler(bind(&EventSub::OnMessage, this, _1, _2));
_client.set_close_handler(bind(&EventSub::OnClose, this, _1));
_client.set_fail_handler(bind(&EventSub::OnFail, this, _1));
#ifndef USE_TWITCH_CLI_MOCK
_client.set_tls_init_handler([](websocketpp::connection_hdl) {
return websocketpp::lib::make_shared<asio::ssl::context>(
asio::ssl::context::sslv23_client);
});
#endif
_url = defaultURL.data();
RegisterInstance();
}
EventSub::~EventSub()
{
Disconnect();
UnregisterInstance();
}
static bool setupEventSubMessageClear()
{
AddIntervalResetStep(&EventSub::ClearAllEvents);
return true;
}
bool EventSub::_setupDone = setupEventSubMessageClear();
void EventSub::ClearEvents()
{
std::lock_guard<std::mutex> lock(_messageMtx);
_messages.clear();
}
std::mutex EventSub::_instancesMtx;
std::vector<EventSub *> EventSub::_instances;
void EventSub::RegisterInstance()
{
std::lock_guard<std::mutex> lock(_instancesMtx);
_instances.emplace_back(this);
}
void EventSub::UnregisterInstance()
{
std::lock_guard<std::mutex> lock(_instancesMtx);
auto it = std::remove(_instances.begin(), _instances.end(), this);
_instances.erase(it, _instances.end());
}
void EventSub::ClearAllEvents()
{
std::lock_guard<std::mutex> lock(_instancesMtx);
for (const auto &eventSub : _instances) {
eventSub->ClearEvents();
}
}
void EventSub::ConnectThread()
{
while (!_disconnect) {
std::unique_lock<std::mutex> lock(_waitMtx);
_client.reset();
_connected = true;
websocketpp::lib::error_code ec;
EventSubWSClient::connection_ptr con =
_client.get_connection(_url, ec);
if (ec) {
blog(LOG_INFO, "Twitch EventSub failed: %s",
ec.message().c_str());
} else {
_client.connect(con);
_connection = connection_hdl(con);
_client.run();
}
blog(LOG_INFO,
"Twitch EventSub trying to reconnect to in %d seconds.",
reconnectDelay);
_cv.wait_for(lock, std::chrono::seconds(reconnectDelay));
}
_connected = false;
}
void EventSub::Connect()
{
std::lock_guard<std::mutex> lock(_connectMtx);
if (_connected) {
vblog(LOG_INFO, "Twitch EventSub connect already in progress");
return;
}
_disconnect = true;
if (_thread.joinable()) {
_thread.join();
}
_disconnect = false;
_thread = std::thread(&EventSub::ConnectThread, this);
}
void EventSub::ClearActiveSubscriptions()
{
std::lock_guard<std::mutex> lock(_subscriptionMtx);
_activeSubscriptions.clear();
}
void EventSub::Disconnect()
{
std::lock_guard<std::mutex> lock(_connectMtx);
_disconnect = true;
websocketpp::lib::error_code ec;
_client.close(_connection, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
{
std::unique_lock<std::mutex> waitLock(_waitMtx);
_cv.notify_all();
}
while (_connected) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
_client.close(_connection, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
}
if (_thread.joinable()) {
_thread.join();
}
_connected = false;
ClearActiveSubscriptions();
}
std::vector<Event> EventSub::Events()
{
std::lock_guard<std::mutex> lock(_messageMtx);
return _messages;
}
bool EventSub::SubscriptionIsActive(const std::string &id)
{
std::lock_guard<std::mutex> lock(_subscriptionMtx);
for (const auto &subscription : _activeSubscriptions) {
if (subscription.id == id) {
return true;
}
}
return false;
}
static bool isAlreadySubscribed(const std::set<Subscription> &subscriptions,
const Subscription &newSubsciption)
{
return subscriptions.find(newSubsciption) != subscriptions.end();
}
static void setTransportData(const OBSData &data, const std::string &sessionID)
{
OBSDataAutoRelease transport = obs_data_create();
obs_data_set_string(transport, "method", "websocket");
obs_data_set_string(transport, "session_id", sessionID.c_str());
obs_data_set_obj(data, "transport", transport);
}
static obs_data_t *copyData(const OBSData &data)
{
auto json = obs_data_get_json(data);
if (!json) {
return nullptr;
}
return obs_data_create_from_json(json);
}
std::string EventSub::AddEventSubscribtion(std::shared_ptr<TwitchToken> token,
Subscription subscription)
{
auto eventSub = token->GetEventSub();
if (!eventSub) {
blog(LOG_WARNING, "failed to get Twitch EventSub from token!");
return "";
}
std::lock_guard<std::mutex> lock(eventSub->_subscriptionMtx);
if (!eventSub->_connected) {
std::thread t([eventSub]() { eventSub->Connect(); });
t.detach();
vblog(LOG_INFO, "Twitch EventSub connect started for %s",
token->GetName().c_str());
return "";
}
if (isAlreadySubscribed(eventSub->_activeSubscriptions, subscription)) {
return eventSub->_activeSubscriptions.find(subscription)->id;
}
OBSDataAutoRelease postData = copyData(subscription.data);
setTransportData(postData.Get(), eventSub->_sessionID);
auto result = SendPostRequest(*token, registerSubscriptionURL.data(),
registerSubscriptionPath.data(), {},
postData.Get());
if (result.status != 202) {
vblog(LOG_INFO, "failed to register Twitch EventSub (%d)",
result.status);
return "";
}
OBSDataArrayAutoRelease replyArray =
obs_data_get_array(result.data, "data");
OBSDataAutoRelease replyData = obs_data_array_item(replyArray, 0);
subscription.id = obs_data_get_string(replyData, "id");
eventSub->_activeSubscriptions.emplace(subscription);
return subscription.id;
}
void EventSub::OnOpen(connection_hdl)
{
vblog(LOG_INFO, "Twitch EventSub connection opened");
_connected = true;
}
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);
}
bool EventSub::IsValidMessageID(const std::string &id)
{
auto it = std::find(_messageIDs.begin(), _messageIDs.end(), id);
if (it != _messageIDs.end()) {
return false;
}
if (!_messageIDs.empty()) {
_messageIDs.pop_front();
}
_messageIDs.push_back(id);
return true;
}
bool EventSub::IsValidID(const std::string &id)
{
return !_sessionID.empty() && id == _sessionID;
}
void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
{
if (!message) {
return;
}
if (message->get_opcode() != websocketpp::frame::opcode::text) {
return;
}
std::string payload = message->get_payload();
OBSDataAutoRelease json = obs_data_create_from_json(payload.c_str());
if (!json) {
blog(LOG_ERROR, "invalid JSON payload received for '%s'",
payload.c_str());
return;
}
OBSDataAutoRelease metadata = obs_data_get_obj(json, "metadata");
std::string timestamp =
obs_data_get_string(metadata, "message_timestamp");
if (!isValidTimestamp(timestamp)) {
blog(LOG_WARNING,
"Discarding Twitch EventSub with invalid timestamp");
return;
}
std::string id = obs_data_get_string(metadata, "message_id");
if (!IsValidMessageID(id)) {
blog(LOG_WARNING,
"Discarding Twitch EventSub with invalid message_id");
return;
}
std::string messageType = obs_data_get_string(metadata, "message_type");
OBSDataAutoRelease payloadJson = obs_data_get_obj(json, "payload");
if (messageType == "session_welcome") {
HandleWelcome(payloadJson);
} else if (messageType == "session_keepalive") {
HandleKeepAlive();
} else if (messageType == "notification") {
HandleNotification(payloadJson);
} else if (messageType == "session_reconnect") {
HandleReconnect(payloadJson);
} else if (messageType == "revocation") {
HanldeRevocation(payloadJson);
} else {
vblog(LOG_INFO, "ignoring message of unknown type '%s'",
messageType.c_str());
}
}
void EventSub::HandleWelcome(obs_data_t *data)
{
OBSDataAutoRelease session = obs_data_get_obj(data, "session");
_sessionID = obs_data_get_string(session, "id");
blog(LOG_INFO, "Twitch EventSub connected");
}
void EventSub::HandleKeepAlive() const
{
// Nothing to do
}
void EventSub::HandleNotification(obs_data_t *data)
{
Event event;
OBSDataAutoRelease subscription =
obs_data_get_obj(data, "subscription");
event.id = obs_data_get_string(subscription, "id");
event.type = obs_data_get_string(subscription, "type");
OBSDataAutoRelease eventData = obs_data_get_obj(data, "event");
event.data = eventData;
std::lock_guard<std::mutex> lock(_messageMtx);
_messages.emplace_back(event);
}
void EventSub::HandleReconnect(obs_data_t *data)
{
OBSDataAutoRelease session = obs_data_get_obj(data, "session");
auto id = obs_data_get_string(session, "id");
if (!IsValidID(id)) {
vblog(LOG_INFO,
"ignoring Twitch EventSub reconnect message with invalid id");
return;
}
_url = obs_data_get_string(session, "reconnect_url");
websocketpp::lib::error_code ec;
_client.close(_connection, websocketpp::close::status::normal,
"Twitch EventSub reconnecting", ec);
}
void EventSub::HanldeRevocation(obs_data_t *data)
{
OBSDataAutoRelease subscription =
obs_data_get_obj(data, "subscription");
auto id = obs_data_get_string(subscription, "id");
auto status = obs_data_get_string(subscription, "status");
auto type = obs_data_get_string(subscription, "type");
auto version = obs_data_get_string(subscription, "version");
OBSDataAutoRelease condition =
obs_data_get_obj(subscription, "condition");
auto conditionJson = obs_data_get_json(condition);
blog(LOG_INFO,
"Twitch EventSub revoked:\n"
"id: %s\n"
"status: %s\n"
"type: %s\n"
"version: %s\n"
"condition: %s\n",
id, status, type, version, conditionJson ? conditionJson : "");
std::lock_guard<std::mutex> lock(_subscriptionMtx);
for (auto it = _activeSubscriptions.begin();
it != _activeSubscriptions.begin();) {
if (it->id == id) {
it = _activeSubscriptions.erase(it);
} else {
++it;
}
}
}
void EventSub::OnClose(connection_hdl hdl)
{
EventSubWSClient::connection_ptr con = _client.get_con_from_hdl(hdl);
auto msg = con->get_ec().message();
blog(LOG_INFO, "Twitch EventSub connection closed: %s", msg.c_str());
ClearActiveSubscriptions();
_connected = false;
}
void EventSub::OnFail(connection_hdl hdl)
{
EventSubWSClient::connection_ptr con = _client.get_con_from_hdl(hdl);
auto msg = con->get_ec().message();
blog(LOG_INFO, "Twitch EventSub connection failed: %s", msg.c_str());
ClearActiveSubscriptions();
_connected = false;
}
bool Subscription::operator<(const Subscription &other) const
{
auto json = obs_data_get_json(data);
std::string jsonString = json ? json : "";
auto otherJson = obs_data_get_json(other.data);
std::string otherJsonString = otherJson ? otherJson : "";
return jsonString < otherJsonString;
}
std::string Event::ToString() const
{
auto json = obs_data_get_json(data);
return json ? json : "";
}
} // namespace advss

View File

@@ -0,0 +1,100 @@
#pragma once
#include <obs.hpp>
#include <websocketpp/client.hpp>
#include <QObject>
#include <mutex>
#include <condition_variable>
#include <set>
#ifdef USE_TWITCH_CLI_MOCK
#include <websocketpp/config/asio_no_tls_client.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
#else
#include <websocketpp/config/asio_client.hpp>
#include <websocketpp/config/asio.hpp>
#endif
namespace advss {
#ifdef USE_TWITCH_CLI_MOCK
typedef websocketpp::client<websocketpp::config::asio_client> EventSubWSClient;
#else
typedef websocketpp::client<websocketpp::config::asio_tls_client>
EventSubWSClient;
#endif
using websocketpp::connection_hdl;
class TwitchToken;
struct Event {
std::string id;
std::string type;
OBSData data;
std::string ToString() const;
};
struct Subscription {
OBSData data;
std::string id;
bool operator<(const Subscription &) const;
};
class EventSub : public QObject {
public:
explicit EventSub();
virtual ~EventSub();
void Connect();
void Disconnect();
std::vector<Event> Events();
bool SubscriptionIsActive(const std::string &id);
static std::string AddEventSubscribtion(std::shared_ptr<TwitchToken>,
Subscription);
static void ClearAllEvents();
void ClearActiveSubscriptions();
private:
void OnOpen(connection_hdl hdl);
void OnMessage(connection_hdl hdl,
EventSubWSClient::message_ptr message);
void OnClose(connection_hdl hdl);
void OnFail(connection_hdl hdl);
void ConnectThread();
bool IsValidMessageID(const std::string &);
bool IsValidID(const std::string &);
void HandleWelcome(obs_data_t *);
void HandleKeepAlive() const;
void HandleNotification(obs_data_t *);
void HandleReconnect(obs_data_t *);
void HanldeRevocation(obs_data_t *);
void ClearEvents();
void RegisterInstance();
void UnregisterInstance();
EventSubWSClient _client;
connection_hdl _connection;
std::thread _thread;
std::mutex _waitMtx;
std::mutex _connectMtx;
std::condition_variable _cv;
std::atomic_bool _connected{false};
std::atomic_bool _disconnect{false};
std::string _url;
std::string _sessionID;
std::mutex _messageMtx;
std::vector<Event> _messages;
std::deque<std::string> _messageIDs;
std::mutex _subscriptionMtx;
std::set<Subscription> _activeSubscriptions;
static std::mutex _instancesMtx;
static std::vector<EventSub *> _instances;
static bool _setupDone;
};
} // namespace advss

View File

@@ -13,17 +13,61 @@ bool MacroActionTwitch::_registered = MacroActionFactory::Register(
{MacroActionTwitch::Create, MacroActionTwitchEdit::Create,
"AdvSceneSwitcher.action.twitch"});
std::string MacroActionTwitch::GetShortDesc() const
{
return GetWeakTwitchTokenName(_token);
}
const static std::map<MacroActionTwitch::Action, std::string> actionTypes = {
{MacroActionTwitch::Action::TITLE,
"AdvSceneSwitcher.action.twitch.type.title"},
{MacroActionTwitch::Action::CATEGORY,
"AdvSceneSwitcher.action.twitch.type.category"},
{MacroActionTwitch::Action::MARKER,
"AdvSceneSwitcher.action.twitch.type.marker"},
{MacroActionTwitch::Action::CLIP,
"AdvSceneSwitcher.action.twitch.type.clip"},
{MacroActionTwitch::Action::COMMERCIAL,
"AdvSceneSwitcher.action.twitch.type.commercial"},
{MacroActionTwitch::Action::CHANNEL_INFO_TITLE_SET,
"AdvSceneSwitcher.action.twitch.type.channel.info.title.set"},
{MacroActionTwitch::Action::CHANNEL_INFO_CATEGORY_SET,
"AdvSceneSwitcher.action.twitch.type.channel.info.category.set"},
{MacroActionTwitch::Action::RAID_START,
"AdvSceneSwitcher.action.twitch.type.raid.start"},
{MacroActionTwitch::Action::COMMERCIAL_START,
"AdvSceneSwitcher.action.twitch.type.commercial.start"},
{MacroActionTwitch::Action::MARKER_CREATE,
"AdvSceneSwitcher.action.twitch.type.marker.create"},
{MacroActionTwitch::Action::CLIP_CREATE,
"AdvSceneSwitcher.action.twitch.type.clip.create"},
{MacroActionTwitch::Action::CHAT_ANNOUNCEMENT_SEND,
"AdvSceneSwitcher.action.twitch.type.chat.announcement.send"},
{MacroActionTwitch::Action::CHAT_EMOTE_ONLY_ENABLE,
"AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable"},
{MacroActionTwitch::Action::CHAT_EMOTE_ONLY_DISABLE,
"AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable"},
{MacroActionTwitch::Action::SEND_CHAT_MESSAGE,
"AdvSceneSwitcher.action.twitch.type.sendChatMessage"},
};
const static std::map<MacroActionTwitch::AnnouncementColor, std::string>
announcementColors = {
{MacroActionTwitch::AnnouncementColor::PRIMARY,
"AdvSceneSwitcher.action.twitch.announcement.primary"},
{MacroActionTwitch::AnnouncementColor::BLUE,
"AdvSceneSwitcher.action.twitch.announcement.blue"},
{MacroActionTwitch::AnnouncementColor::GREEN,
"AdvSceneSwitcher.action.twitch.announcement.green"},
{MacroActionTwitch::AnnouncementColor::ORANGE,
"AdvSceneSwitcher.action.twitch.announcement.orange"},
{MacroActionTwitch::AnnouncementColor::PURPLE,
"AdvSceneSwitcher.action.twitch.announcement.purple"},
};
const static std::map<MacroActionTwitch::AnnouncementColor, std::string>
announcementColorsTwitch = {
{MacroActionTwitch::AnnouncementColor::PRIMARY, "primary"},
{MacroActionTwitch::AnnouncementColor::BLUE, "blue"},
{MacroActionTwitch::AnnouncementColor::GREEN, "green"},
{MacroActionTwitch::AnnouncementColor::ORANGE, "orange"},
{MacroActionTwitch::AnnouncementColor::PURPLE, "purple"},
};
const static std::map<MacroActionTwitch::RedemptionStatus, std::string>
redemptionStatusesTwitch = {
{MacroActionTwitch::RedemptionStatus::CANCELED, "CANCELED"},
{MacroActionTwitch::RedemptionStatus::FULFILLED, "FULFILLED"},
};
void MacroActionTwitch::SetStreamTitle(
@@ -35,11 +79,10 @@ void MacroActionTwitch::SetStreamTitle(
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "title", _streamTitle.c_str());
auto result = SendPatchRequest(
"https://api.twitch.tv",
std::string("/helix/channels?broadcaster_id=") +
token->GetUserID(),
*token, data.Get());
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 title! (%d)",
@@ -57,11 +100,11 @@ void MacroActionTwitch::SetStreamCategory(
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "game_id",
std::to_string(_category.id).c_str());
auto result = SendPatchRequest(
"https://api.twitch.tv",
std::string("/helix/channels?broadcaster_id=") +
token->GetUserID(),
*token, data.Get());
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 category! (%d)",
result.status);
@@ -79,9 +122,8 @@ void MacroActionTwitch::CreateStreamMarker(
_markerDescription.c_str());
}
auto result = SendPostRequest("https://api.twitch.tv",
"/helix/streams/markers", *token,
data.Get());
auto result = SendPostRequest(*token, "https://api.twitch.tv",
"/helix/streams/markers", {}, data.Get());
if (result.status != 200) {
blog(LOG_INFO, "Failed to create marker! (%d)", result.status);
@@ -91,13 +133,12 @@ void MacroActionTwitch::CreateStreamMarker(
void MacroActionTwitch::CreateStreamClip(
const std::shared_ptr<TwitchToken> &token) const
{
OBSDataAutoRelease data = obs_data_create();
auto hasDelay = _clipHasDelay ? "true" : "false";
auto result = SendPostRequest(
"https://api.twitch.tv",
"/helix/clips?broadcaster_id=" + token->GetUserID() +
"&has_delay=" + hasDelay,
*token, data.Get());
auto result = SendPostRequest(*token, "https://api.twitch.tv",
"/helix/clips",
{{"broadcaster_id", token->GetUserID()},
{"has_delay", hasDelay}});
if (result.status != 202) {
blog(LOG_INFO, "Failed to create clip! (%d)", result.status);
@@ -110,9 +151,10 @@ void MacroActionTwitch::StartCommercial(
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "broadcaster_id", token->GetUserID().c_str());
obs_data_set_int(data, "length", _duration.Seconds());
auto result = SendPostRequest("https://api.twitch.tv",
"/helix/channels/commercial", *token,
auto result = SendPostRequest(*token, "https://api.twitch.tv",
"/helix/channels/commercial", {},
data.Get());
if (result.status != 200) {
OBSDataArrayAutoRelease replyArray =
obs_data_get_array(result.data, "data");
@@ -120,15 +162,81 @@ void MacroActionTwitch::StartCommercial(
obs_data_array_item(replyArray, 0);
blog(LOG_INFO,
"Failed to start commercial! (%d)\n"
"length: %d\n"
"length: %lld\n"
"message: %s\n"
"retry_after: %d\n",
"retry_after: %lld\n",
result.status, obs_data_get_int(replyData, "length"),
obs_data_get_string(replyData, "message"),
obs_data_get_int(replyData, "retry_after"));
}
}
void MacroActionTwitch::SendChatAnnouncement(
const std::shared_ptr<TwitchToken> &token) const
{
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "message", _announcementMessage.c_str());
obs_data_set_string(
data, "color",
announcementColorsTwitch.at(_announcementColor).c_str());
auto userId = token->GetUserID();
auto result = SendPostRequest(
*token, "https://api.twitch.tv", "/helix/chat/announcements",
{{"broadcaster_id", userId}, {"moderator_id", userId}},
data.Get());
if (result.status != 204) {
blog(LOG_INFO, "Failed to send chat announcement! (%d)",
result.status);
}
}
void MacroActionTwitch::SetChatEmoteOnlyMode(
const std::shared_ptr<TwitchToken> &token, bool enable) const
{
OBSDataAutoRelease data = obs_data_create();
obs_data_set_bool(data, "emote_mode", enable);
auto userId = token->GetUserID();
auto result = SendPatchRequest(
*token, "https://api.twitch.tv", "/helix/chat/settings",
{{"broadcaster_id", userId}, {"moderator_id", userId}},
data.Get());
if (result.status != 200) {
blog(LOG_INFO, "Failed to %s chat's emote-only mode! (%d)",
enable ? "enable" : "disable", result.status);
}
}
void MacroActionTwitch::StartRaid(const std::shared_ptr<TwitchToken> &token)
{
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "from_broadcaster_id",
token->GetUserID().c_str());
obs_data_set_string(data, "to_broadcaster_id",
_channel.GetUserID(*token).c_str());
auto result = SendPostRequest(*token, "https://api.twitch.tv",
"/helix/raids", {}, data.Get());
if (result.status != 200) {
blog(LOG_INFO, "Failed to start raid! (%d)\n", result.status);
}
}
void MacroActionTwitch::SendChatMessage(
const std::shared_ptr<TwitchToken> &token)
{
if (!_chatConnection) {
_chatConnection = TwitchChatConnection::GetChatConnection(
*token, _channel);
return;
}
_chatConnection->SendChatMessage(_chatMessage);
}
bool MacroActionTwitch::PerformAction()
{
auto token = _token.lock();
@@ -137,20 +245,34 @@ bool MacroActionTwitch::PerformAction()
}
switch (_action) {
case MacroActionTwitch::Action::TITLE:
case Action::CHANNEL_INFO_TITLE_SET:
SetStreamTitle(token);
break;
case MacroActionTwitch::Action::CATEGORY:
case Action::CHANNEL_INFO_CATEGORY_SET:
SetStreamCategory(token);
break;
case MacroActionTwitch::Action::MARKER:
case Action::RAID_START:
StartRaid(token);
break;
case Action::COMMERCIAL_START:
StartCommercial(token);
break;
case Action::MARKER_CREATE:
CreateStreamMarker(token);
break;
case MacroActionTwitch::Action::CLIP:
case Action::CLIP_CREATE:
CreateStreamClip(token);
break;
case MacroActionTwitch::Action::COMMERCIAL:
StartCommercial(token);
case Action::CHAT_ANNOUNCEMENT_SEND:
SendChatAnnouncement(token);
break;
case Action::CHAT_EMOTE_ONLY_ENABLE:
SetChatEmoteOnlyMode(token, true);
break;
case Action::CHAT_EMOTE_ONLY_DISABLE:
SetChatEmoteOnlyMode(token, false);
case MacroActionTwitch::Action::SEND_CHAT_MESSAGE:
SendChatMessage(token);
break;
default:
break;
@@ -175,6 +297,7 @@ void MacroActionTwitch::LogAction() const
bool MacroActionTwitch::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
obs_data_set_int(obj, "action", static_cast<int>(_action));
obs_data_set_string(obj, "token",
GetWeakTwitchTokenName(_token).c_str());
@@ -183,12 +306,19 @@ bool MacroActionTwitch::Save(obs_data_t *obj) const
_markerDescription.Save(obj, "markerDescription");
obs_data_set_bool(obj, "clipHasDelay", _clipHasDelay);
_duration.Save(obj);
_announcementMessage.Save(obj, "announcementMessage");
obs_data_set_int(obj, "announcementColor",
static_cast<int>(_announcementColor));
_channel.Save(obj);
_chatMessage.Save(obj, "chatMessage");
return true;
}
bool MacroActionTwitch::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
_token = GetWeakTwitchTokenByName(obs_data_get_string(obj, "token"));
_streamTitle.Load(obj, "streamTitle");
@@ -196,35 +326,143 @@ bool MacroActionTwitch::Load(obs_data_t *obj)
_markerDescription.Load(obj, "markerDescription");
_clipHasDelay = obs_data_get_bool(obj, "clipHasDelay");
_duration.Load(obj);
return true;
}
_announcementMessage.Load(obj, "announcementMessage");
_announcementColor = static_cast<AnnouncementColor>(
obs_data_get_int(obj, "announcementColor"));
_channel.Load(obj);
_chatMessage.Load(obj, "chatMessage");
ResetChatConnection();
std::string MacroActionTwitch::GetShortDesc() const
{
return GetWeakTwitchTokenName(_token);
return true;
}
bool MacroActionTwitch::ActionIsSupportedByToken()
{
static const std::unordered_map<Action, TokenOption> requiredOption = {
{Action::TITLE, {"channel:manage:broadcast"}},
{Action::CATEGORY, {"channel:manage:broadcast"}},
{Action::MARKER, {"channel:manage:broadcast"}},
{Action::CLIP, {"clips:edit"}},
{Action::COMMERCIAL, {"channel:edit:commercial"}},
{Action::CHANNEL_INFO_TITLE_SET, {"channel:manage:broadcast"}},
{Action::CHANNEL_INFO_CATEGORY_SET,
{"channel:manage:broadcast"}},
{Action::CHANNEL_INFO_LANGUAGE_SET,
{"channel:manage:broadcast"}},
{Action::CHANNEL_INFO_DELAY_SET, {"channel:manage:broadcast"}},
{Action::CHANNEL_INFO_BRANDED_CONTENT_ENABLE,
{"channel:manage:broadcast"}},
{Action::CHANNEL_INFO_BRANDED_CONTENT_DISABLE,
{"channel:manage:broadcast"}},
{Action::RAID_START, {"channel:manage:raids"}},
{Action::RAID_END, {"channel:manage:raids"}},
{Action::SHOUTOUT_SEND, {"moderator:manage:shoutouts"}},
{Action::POLL_END, {"channel:manage:polls"}},
{Action::PREDICTION_END, {"channel:manage:predictions"}},
{Action::SHIELD_MODE_START, {"moderator:manage:shield_mode"}},
{Action::SHIELD_MODE_END, {"moderator:manage:shield_mode"}},
{Action::POINTS_REWARD_ENABLE, {"channel:manage:redemptions"}},
{Action::POINTS_REWARD_DISABLE, {"channel:manage:redemptions"}},
{Action::POINTS_REWARD_PAUSE, {"channel:manage:redemptions"}},
{Action::POINTS_REWARD_UNPAUSE, {"channel:manage:redemptions"}},
{Action::POINTS_REWARD_TITLE_SET,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_PROMPT_SET,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_COST_SET,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_USER_INPUT_REQUIRE,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_USER_INPUT_UNREQUIRE,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_COOLDOWN_ENABLE,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_COOLDOWN_DISABLE,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_QUEUE_SKIP_ENABLE,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_QUEUE_SKIP_DISABLE,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_MAX_PER_STREAM_ENABLE,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_MAX_PER_STREAM_DISABLE,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_MAX_PER_USER_ENABLE,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_MAX_PER_USER_DISABLE,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_DELETE, {"channel:manage:redemptions"}},
{Action::POINTS_REWARD_REDEMPTION_CANCEL,
{"channel:manage:redemptions"}},
{Action::POINTS_REWARD_REDEMPTION_FULFILL,
{"channel:manage:redemptions"}},
{Action::USER_BAN, {"moderator:manage:banned_users"}},
{Action::USER_UNBAN, {"moderator:manage:banned_users"}},
{Action::USER_BLOCK, {"user:manage:blocked_users"}},
{Action::USER_UNBLOCK, {"user:manage:blocked_users"}},
{Action::USER_MODERATOR_ADD, {"channel:manage:moderators"}},
{Action::USER_MODERATOR_DELETE, {"channel:manage:moderators"}},
{Action::USER_VIP_ADD, {"channel:manage:vips"}},
{Action::USER_VIP_DELETE, {"channel:manage:vips"}},
{Action::COMMERCIAL_START, {"channel:edit:commercial"}},
{Action::COMMERCIAL_SNOOZE, {"channel:manage:ads"}},
{Action::MARKER_CREATE, {"channel:manage:broadcast"}},
{Action::CLIP_CREATE, {"clips:edit"}},
{Action::CHAT_ANNOUNCEMENT_SEND,
{"moderator:manage:announcements"}},
{Action::CHAT_EMOTE_ONLY_ENABLE,
{"moderator:manage:chat_settings"}},
{Action::CHAT_EMOTE_ONLY_DISABLE,
{"moderator:manage:chat_settings"}},
{Action::CHAT_FOLLOWER_ONLY_ENABLE,
{"moderator:manage:chat_settings"}},
{Action::CHAT_FOLLOWER_ONLY_DISABLE,
{"moderator:manage:chat_settings"}},
{Action::CHAT_SUBSCRIBER_ONLY_ENABLE,
{"moderator:manage:chat_settings"}},
{Action::CHAT_SUBSCRIBER_ONLY_DISABLE,
{"moderator:manage:chat_settings"}},
{Action::CHAT_SLOW_MODE_ENABLE,
{"moderator:manage:chat_settings"}},
{Action::CHAT_SLOW_MODE_DISABLE,
{"moderator:manage:chat_settings"}},
{Action::CHAT_NON_MODERATOR_DELAY_ENABLE,
{"moderator:manage:chat_settings"}},
{Action::CHAT_NON_MODERATOR_DELAY_DISABLE,
{"moderator:manage:chat_settings"}},
{Action::CHAT_UNIQUE_MODE_ENABLE,
{"moderator:manage:chat_settings"}},
{Action::CHAT_UNIQUE_MODE_DISABLE,
{"moderator:manage:chat_settings"}},
{Action::WHISPER_SEND, {"user:manage:whispers"}},
{Action::SEND_CHAT_MESSAGE, {"chat:edit"}},
};
auto token = _token.lock();
if (!token) {
return false;
}
auto option = requiredOption.find(_action);
assert(option != requiredOption.end());
if (option == requiredOption.end()) {
return false;
}
return token->OptionIsEnabled(option->second);
}
void MacroActionTwitch::ResetChatConnection()
{
_chatConnection.reset();
}
static inline void populateActionSelection(QComboBox *list)
{
for (const auto &[_, name] : actionTypes) {
for (const auto &[action, name] : actionTypes) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(action));
}
}
static inline void populateAnnouncementColorSelection(QComboBox *list)
{
for (const auto &[_, name] : announcementColors) {
list->addItem(obs_module_text(name.c_str()));
}
}
@@ -232,73 +470,61 @@ static inline void populateActionSelection(QComboBox *list)
MacroActionTwitchEdit::MacroActionTwitchEdit(
QWidget *parent, std::shared_ptr<MacroActionTwitch> entryData)
: QWidget(parent),
_actions(new QComboBox()),
_layout(new QHBoxLayout()),
_actions(new FilterComboBox()),
_tokens(new TwitchConnectionSelection()),
_tokenPermissionWarning(new QLabel(obs_module_text(
"AdvSceneSwitcher.twitchToken.permissionsInsufficient"))),
_streamTitle(new VariableLineEdit(this)),
_category(new TwitchCategorySelection(this)),
_manualCategorySearch(new TwitchCategorySearchButton()),
_category(new TwitchCategoryWidget(this)),
_markerDescription(new VariableLineEdit(this)),
_clipHasDelay(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.twitch.clip.hasDelay"))),
_duration(new DurationSelection(this, false, 0)),
_layout(new QHBoxLayout()),
_tokenPermissionWarning(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient")))
_announcementMessage(new VariableTextEdit(this)),
_announcementColor(new QComboBox(this)),
_channel(new TwitchChannelSelection(this)),
_chatMessage(new VariableTextEdit(this))
{
_streamTitle->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_streamTitle->setMaxLength(140);
_markerDescription->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_markerDescription->setMaxLength(140);
SetWidgetProperties();
SetWidgetSignalConnections();
auto spinBox = _duration->SpinBox();
spinBox->setSuffix("s");
spinBox->setMaximum(180);
populateActionSelection(_actions);
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionChanged(int)));
QWidget::connect(_tokens, SIGNAL(SelectionChanged(const QString &)),
this, SLOT(TwitchTokenChanged(const QString &)));
QWidget::connect(_streamTitle, SIGNAL(editingFinished()), this,
SLOT(StreamTitleChanged()));
QWidget::connect(_category,
SIGNAL(CategoreyChanged(const TwitchCategory &)), this,
SLOT(CategoreyChanged(const TwitchCategory &)));
QWidget::connect(_markerDescription, SIGNAL(editingFinished()), this,
SLOT(MarkerDescriptionChanged()));
QObject::connect(_clipHasDelay, SIGNAL(stateChanged(int)), this,
SLOT(HasClipDelayChanged(const Duration &)));
QObject::connect(_duration, SIGNAL(DurationChanged(const Duration &)),
this, SLOT(DurationChanged(const Duration &)));
QWidget::connect(&_tokenPermissionCheckTimer, SIGNAL(timeout()), this,
SLOT(CheckTokenPermissions()));
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.twitch.entry"),
_layout,
{{"{{account}}", _tokens},
{"{{actions}}", _actions},
{"{{streamTitle}}", _streamTitle},
{"{{category}}", _category},
{"{{manualCategorySearch}}", _manualCategorySearch},
{"{{markerDescription}}", _markerDescription},
{"{{clipHasDelay}}", _clipHasDelay},
{"{{duration}}", _duration}});
_layout->setContentsMargins(0, 0, 0, 0);
_entryData = entryData;
SetWidgetLayout();
auto mainLayout = new QVBoxLayout();
mainLayout->addLayout(_layout);
mainLayout->addWidget(_announcementMessage);
mainLayout->addWidget(_chatMessage);
mainLayout->addWidget(_tokenPermissionWarning);
setLayout(mainLayout);
_tokenPermissionCheckTimer.start(1000);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionTwitchEdit::ActionChanged(int idx)
{
if (_loading || !_entryData) {
return;
}
if (idx == -1) { // Reset to previous selection
const QSignalBlocker b(_actions);
_actions->setCurrentIndex(_actions->findData(
static_cast<int>(_entryData->_action)));
return;
}
auto lock = LockContext();
_entryData->_action = static_cast<MacroActionTwitch::Action>(
_actions->itemData(idx).toInt());
SetWidgetLayout();
SetWidgetVisibility();
}
void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token)
{
if (_loading || !_entryData) {
@@ -308,11 +534,20 @@ void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token)
auto lock = LockContext();
_entryData->_token = GetWeakTwitchTokenByQString(token);
_category->SetToken(_entryData->_token);
_manualCategorySearch->SetToken(_entryData->_token);
SetupWidgetVisibility();
_channel->SetToken(_entryData->_token);
SetWidgetVisibility();
emit(HeaderInfoChanged(token));
}
void MacroActionTwitchEdit::CheckTokenPermissions()
{
_tokenPermissionWarning->setVisible(
_entryData && !_entryData->ActionIsSupportedByToken());
adjustSize();
updateGeometry();
}
void MacroActionTwitchEdit::StreamTitleChanged()
{
if (_loading || !_entryData) {
@@ -364,31 +599,111 @@ void MacroActionTwitchEdit::DurationChanged(const Duration &duration)
_entryData->_duration = duration;
}
void MacroActionTwitchEdit::CheckTokenPermissions()
void MacroActionTwitchEdit::AnnouncementMessageChanged()
{
_tokenPermissionWarning->setVisible(
_entryData && !_entryData->ActionIsSupportedByToken());
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_announcementMessage =
_announcementMessage->toPlainText().toStdString();
adjustSize();
updateGeometry();
}
void MacroActionTwitchEdit::SetupWidgetVisibility()
void MacroActionTwitchEdit::AnnouncementColorChanged(int index)
{
_streamTitle->setVisible(_entryData->_action ==
MacroActionTwitch::Action::TITLE);
_category->setVisible(_entryData->_action ==
MacroActionTwitch::Action::CATEGORY);
_manualCategorySearch->setVisible(_entryData->_action ==
MacroActionTwitch::Action::CATEGORY);
_markerDescription->setVisible(_entryData->_action ==
MacroActionTwitch::Action::MARKER);
_clipHasDelay->setVisible(_entryData->_action ==
MacroActionTwitch::Action::CLIP);
_duration->setVisible(_entryData->_action ==
MacroActionTwitch::Action::COMMERCIAL);
if (_loading || !_entryData) {
return;
}
if (_entryData->_action == MacroActionTwitch::Action::TITLE ||
_entryData->_action == MacroActionTwitch::Action::MARKER) {
auto lock = LockContext();
_entryData->_announcementColor =
static_cast<MacroActionTwitch::AnnouncementColor>(index);
}
void MacroActionTwitchEdit::SetWidgetProperties()
{
_streamTitle->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_streamTitle->setMaxLength(140);
_markerDescription->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_markerDescription->setMaxLength(140);
_announcementMessage->setMaxLength(500);
auto durationSpinBox = _duration->SpinBox();
durationSpinBox->setMaximum(180);
durationSpinBox->setSuffix("s");
populateActionSelection(_actions);
populateAnnouncementColorSelection(_announcementColor);
}
void MacroActionTwitchEdit::SetWidgetSignalConnections()
{
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ActionChanged(int)));
QWidget::connect(_tokens, SIGNAL(SelectionChanged(const QString &)),
this, SLOT(TwitchTokenChanged(const QString &)));
QWidget::connect(&_tokenPermissionCheckTimer, SIGNAL(timeout()), this,
SLOT(CheckTokenPermissions()));
QWidget::connect(_streamTitle, SIGNAL(editingFinished()), this,
SLOT(StreamTitleChanged()));
QWidget::connect(_category,
SIGNAL(CategoreyChanged(const TwitchCategory &)), this,
SLOT(CategoreyChanged(const TwitchCategory &)));
QWidget::connect(_markerDescription, SIGNAL(editingFinished()), this,
SLOT(MarkerDescriptionChanged()));
QObject::connect(_clipHasDelay, SIGNAL(stateChanged(int)), this,
SLOT(ClipHasDelayChanged(int)));
QObject::connect(_duration, SIGNAL(DurationChanged(const Duration &)),
this, SLOT(DurationChanged(const Duration &)));
QWidget::connect(_announcementMessage, SIGNAL(textChanged()), this,
SLOT(AnnouncementMessageChanged()));
QWidget::connect(_announcementColor, SIGNAL(currentIndexChanged(int)),
this, SLOT(AnnouncementColorChanged(int)));
QWidget::connect(_channel,
SIGNAL(ChannelChanged(const TwitchChannel &)), this,
SLOT(ChannelChanged(const TwitchChannel &)));
QWidget::connect(_chatMessage, SIGNAL(textChanged()), this,
SLOT(ChatMessageChanged()));
}
void MacroActionTwitchEdit::SetWidgetVisibility()
{
_streamTitle->setVisible(
_entryData->_action ==
MacroActionTwitch::Action::CHANNEL_INFO_TITLE_SET);
_category->setVisible(
_entryData->_action ==
MacroActionTwitch::Action::CHANNEL_INFO_CATEGORY_SET);
_channel->setVisible(
_entryData->_action == MacroActionTwitch::Action::RAID_START ||
_entryData->_action == MacroActionTwitch::Action::RAID_END ||
_entryData->_action ==
MacroActionTwitch::Action::SEND_CHAT_MESSAGE);
_duration->setVisible(_entryData->_action ==
MacroActionTwitch::Action::COMMERCIAL_START);
_markerDescription->setVisible(
_entryData->_action ==
MacroActionTwitch::Action::MARKER_CREATE);
_clipHasDelay->setVisible(_entryData->_action ==
MacroActionTwitch::Action::CLIP_CREATE);
_announcementMessage->setVisible(
_entryData->_action ==
MacroActionTwitch::Action::CHAT_ANNOUNCEMENT_SEND);
_announcementColor->setVisible(
_entryData->_action ==
MacroActionTwitch::Action::CHAT_ANNOUNCEMENT_SEND);
_chatMessage->setVisible(_entryData->_action ==
MacroActionTwitch::Action::SEND_CHAT_MESSAGE);
if (_entryData->_action ==
MacroActionTwitch::Action::CHANNEL_INFO_TITLE_SET ||
_entryData->_action == MacroActionTwitch::Action::MARKER_CREATE) {
RemoveStretchIfPresent(_layout);
} else {
AddStretchIfNecessary(_layout);
@@ -401,33 +716,83 @@ void MacroActionTwitchEdit::SetupWidgetVisibility()
updateGeometry();
}
void MacroActionTwitchEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_tokens->SetToken(_entryData->_token);
_streamTitle->setText(_entryData->_streamTitle);
_category->SetToken(_entryData->_token);
_manualCategorySearch->SetToken(_entryData->_token);
_category->SetCategory(_entryData->_category);
_markerDescription->setText(_entryData->_markerDescription);
_clipHasDelay->setChecked(_entryData->_clipHasDelay);
_duration->SetDuration(_entryData->_duration);
SetupWidgetVisibility();
}
void MacroActionTwitchEdit::ActionChanged(int value)
void MacroActionTwitchEdit::ChannelChanged(const TwitchChannel &channel)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_action = static_cast<MacroActionTwitch::Action>(value);
SetupWidgetVisibility();
_entryData->_channel = channel;
}
void MacroActionTwitchEdit::ChatMessageChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_chatMessage = _chatMessage->toPlainText().toStdString();
adjustSize();
updateGeometry();
}
void MacroActionTwitchEdit::SetWidgetLayout()
{
const std::vector<QWidget *> widgets{
_tokens, _actions, _streamTitle,
_category, _markerDescription, _clipHasDelay,
_duration, _announcementColor, _channel};
for (auto widget : widgets) {
_layout->removeWidget(widget);
}
ClearLayout(_layout);
auto layoutText =
_entryData->_action ==
MacroActionTwitch::Action::SEND_CHAT_MESSAGE
? obs_module_text(
"AdvSceneSwitcher.action.twitch.entry.chat")
: obs_module_text(
"AdvSceneSwitcher.action.twitch.entry.default");
PlaceWidgets(layoutText, _layout,
{{"{{account}}", _tokens},
{"{{actions}}", _actions},
{"{{streamTitle}}", _streamTitle},
{"{{category}}", _category},
{"{{markerDescription}}", _markerDescription},
{"{{clipHasDelay}}", _clipHasDelay},
{"{{duration}}", _duration},
{"{{announcementColor}}", _announcementColor},
{"{{channel}}", _channel}});
_layout->setContentsMargins(0, 0, 0, 0);
}
void MacroActionTwitchEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_actions->setCurrentIndex(
_actions->findData(static_cast<int>(_entryData->_action)));
_tokens->SetToken(_entryData->_token);
_streamTitle->setText(_entryData->_streamTitle);
_category->SetToken(_entryData->_token);
_category->SetCategory(_entryData->_category);
_markerDescription->setText(_entryData->_markerDescription);
_clipHasDelay->setChecked(_entryData->_clipHasDelay);
_duration->SetDuration(_entryData->_duration);
_announcementMessage->setPlainText(_entryData->_announcementMessage);
_announcementColor->setCurrentIndex(
static_cast<int>(_entryData->_announcementColor));
_channel->SetToken(_entryData->_token);
_channel->SetChannel(_entryData->_channel);
_chatMessage->setPlainText(_entryData->_chatMessage);
SetWidgetVisibility();
}
} // namespace advss

View File

@@ -2,8 +2,12 @@
#include "macro-action-edit.hpp"
#include "token.hpp"
#include "category-selection.hpp"
#include "channel-selection.hpp"
#include "chat-connection.hpp"
#include "points-reward-selection.hpp"
#include <variable-line-edit.hpp>
#include <variable-text-edit.hpp>
#include <duration-control.hpp>
namespace advss {
@@ -11,27 +15,146 @@ namespace advss {
class MacroActionTwitch : public MacroAction {
public:
MacroActionTwitch(Macro *m) : MacroAction(m) {}
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)
{
return std::make_shared<MacroActionTwitch>(m);
}
bool ActionIsSupportedByToken();
std::string GetId() const { return id; };
std::string GetShortDesc() const;
enum class Action {
TITLE,
CATEGORY,
MARKER,
CLIP,
COMMERCIAL,
// Channel info update
CHANNEL_INFO_TITLE_SET = 10,
CHANNEL_INFO_CATEGORY_SET = 20,
CHANNEL_INFO_LANGUAGE_SET = 30,
CHANNEL_INFO_DELAY_SET = 40,
CHANNEL_INFO_TAGS_SET = 50, // TODO
CHANNEL_INFO_CONTENT_LABELS_SET = 60, // TODO
CHANNEL_INFO_BRANDED_CONTENT_ENABLE = 70,
CHANNEL_INFO_BRANDED_CONTENT_DISABLE = 71,
// Raid
RAID_START = 110,
RAID_END = 120,
// Shoutout
SHOUTOUT_SEND = 200,
// Poll
POLL_START = 300, // TODO
POLL_END = 310,
// Prediction
PREDICTION_START = 400, // TODO
PREDICTION_END = 410,
// Shield mode
SHIELD_MODE_START = 500,
SHIELD_MODE_END = 501,
// Reward add
POINTS_REWARD_ADD = 600, // TODO
// Reward update
POINTS_REWARD_ENABLE = 710,
POINTS_REWARD_DISABLE = 711,
POINTS_REWARD_PAUSE = 712,
POINTS_REWARD_UNPAUSE = 713,
POINTS_REWARD_TITLE_SET = 720,
POINTS_REWARD_PROMPT_SET = 730,
POINTS_REWARD_COST_SET = 740,
POINTS_REWARD_BACKGROUND_COLOR_SET = 750, // TODO
POINTS_REWARD_USER_INPUT_REQUIRE = 760,
POINTS_REWARD_USER_INPUT_UNREQUIRE = 761,
POINTS_REWARD_COOLDOWN_ENABLE = 770,
POINTS_REWARD_COOLDOWN_DISABLE = 771,
POINTS_REWARD_QUEUE_SKIP_ENABLE = 780,
POINTS_REWARD_QUEUE_SKIP_DISABLE = 781,
POINTS_REWARD_MAX_PER_STREAM_ENABLE = 790,
POINTS_REWARD_MAX_PER_STREAM_DISABLE = 791,
POINTS_REWARD_MAX_PER_USER_ENABLE = 792,
POINTS_REWARD_MAX_PER_USER_DISABLE = 793,
// Reward delete
POINTS_REWARD_DELETE = 800,
// Reward redemption update
POINTS_REWARD_REDEMPTION_FULFILL = 900,
POINTS_REWARD_REDEMPTION_CANCEL = 910,
// User state
USER_BAN = 1000,
USER_UNBAN = 1100,
USER_BLOCK = 1200,
USER_UNBLOCK = 1300,
USER_MODERATOR_ADD = 1400,
USER_MODERATOR_DELETE = 1500,
USER_VIP_ADD = 1600,
USER_VIP_DELETE = 1700,
USER_CHAT_COLOR_UPDATE = 1800, // TODO
// Commercial
COMMERCIAL_START = 1900,
COMMERCIAL_SNOOZE = 2000,
// Marker
MARKER_CREATE = 2100,
// Clips
CLIP_CREATE = 2200,
// Videos
VIDEOS_DELETE = 2300, // TODO
// Chat
CHAT_MESSAGES_DELETE = 2400, // TODO
CHAT_ANNOUNCEMENT_SEND = 2500,
CHAT_EMOTE_ONLY_ENABLE = 2600,
CHAT_EMOTE_ONLY_DISABLE = 2601,
CHAT_FOLLOWER_ONLY_ENABLE = 2602,
CHAT_FOLLOWER_ONLY_DISABLE = 2603,
CHAT_SUBSCRIBER_ONLY_ENABLE = 2604,
CHAT_SUBSCRIBER_ONLY_DISABLE = 2605,
CHAT_SLOW_MODE_ENABLE = 2640,
CHAT_SLOW_MODE_DISABLE = 2641,
CHAT_NON_MODERATOR_DELAY_ENABLE = 2650,
CHAT_NON_MODERATOR_DELAY_DISABLE = 2651,
CHAT_UNIQUE_MODE_ENABLE = 2960,
CHAT_UNIQUE_MODE_DISABLE = 2661,
CHAT_AUTOMOD_UPDATE = 2700, // TODO
CHAT_AUTOMOD_MESSAGE_ALLOW = 2800, // TODO
CHAT_AUTOMOD_MESSAGE_DENY = 2801, // TODO
CHAT_BLOCKED_TERM_ADD = 2900, // TODO
CHAT_BLOCKED_TERM_DELETE = 3000, // TODO
WHISPER_SEND = 3100,
// Schedule
CHANNEL_SCHEDULE_UPDATE = 3200, // TODO
CHANNEL_SCHEDULE_SEGMENT_ADD = 3300, // TODO
CHANNEL_SCHEDULE_SEGMENT_UPDATE = 3400, // TODO
CHANNEL_SCHEDULE_SEGMENT_DELETE = 3500, // TODO
SEND_CHAT_MESSAGE = 5000,
};
Action _action = Action::TITLE;
enum class AnnouncementColor {
PRIMARY,
BLUE,
GREEN,
ORANGE,
PURPLE,
};
enum class RedemptionStatus { CANCELED, FULFILLED };
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
bool ActionIsSupportedByToken();
void ResetChatConnection();
Action _action = Action::CHANNEL_INFO_TITLE_SET;
std::weak_ptr<TwitchToken> _token;
StringVariable _streamTitle =
obs_module_text("AdvSceneSwitcher.action.twitch.title.title");
@@ -40,6 +163,11 @@ public:
"AdvSceneSwitcher.action.twitch.marker.description");
bool _clipHasDelay = false;
Duration _duration = 60;
StringVariable _announcementMessage = obs_module_text(
"AdvSceneSwitcher.action.twitch.announcement.message");
AnnouncementColor _announcementColor = AnnouncementColor::PRIMARY;
TwitchChannel _channel;
StringVariable _chatMessage;
private:
void SetStreamTitle(const std::shared_ptr<TwitchToken> &) const;
@@ -47,6 +175,13 @@ private:
void CreateStreamMarker(const std::shared_ptr<TwitchToken> &) const;
void CreateStreamClip(const std::shared_ptr<TwitchToken> &) const;
void StartCommercial(const std::shared_ptr<TwitchToken> &) const;
void SendChatAnnouncement(const std::shared_ptr<TwitchToken> &) const;
void SetChatEmoteOnlyMode(const std::shared_ptr<TwitchToken> &,
bool enable) const;
void StartRaid(const std::shared_ptr<TwitchToken> &);
void SendChatMessage(const std::shared_ptr<TwitchToken> &);
std::shared_ptr<TwitchChatConnection> _chatConnection;
static bool _registered;
static const std::string id;
@@ -59,7 +194,6 @@ public:
MacroActionTwitchEdit(
QWidget *parent,
std::shared_ptr<MacroActionTwitch> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
@@ -67,16 +201,21 @@ public:
parent,
std::dynamic_pointer_cast<MacroActionTwitch>(action));
}
void UpdateEntryData();
private slots:
void ActionChanged(int);
void TwitchTokenChanged(const QString &);
void CheckTokenPermissions();
void StreamTitleChanged();
void CategoreyChanged(const TwitchCategory &);
void MarkerDescriptionChanged();
void ClipHasDelayChanged(int state);
void DurationChanged(const Duration &);
void CheckTokenPermissions();
void AnnouncementMessageChanged();
void AnnouncementColorChanged(int index);
void ChannelChanged(const TwitchChannel &);
void ChatMessageChanged();
signals:
void HeaderInfoChanged(const QString &);
@@ -85,19 +224,26 @@ protected:
std::shared_ptr<MacroActionTwitch> _entryData;
private:
void SetupWidgetVisibility();
void SetWidgetProperties();
void SetWidgetSignalConnections();
void SetWidgetLayout();
void SetWidgetVisibility();
QComboBox *_actions;
QHBoxLayout *_layout;
FilterComboBox *_actions;
TwitchConnectionSelection *_tokens;
QLabel *_tokenPermissionWarning;
QTimer _tokenPermissionCheckTimer;
VariableLineEdit *_streamTitle;
TwitchCategorySelection *_category;
TwitchCategorySearchButton *_manualCategorySearch;
TwitchCategoryWidget *_category;
VariableLineEdit *_markerDescription;
QCheckBox *_clipHasDelay;
DurationSelection *_duration;
QHBoxLayout *_layout;
QLabel *_tokenPermissionWarning;
QTimer _tokenPermissionCheckTimer;
VariableTextEdit *_announcementMessage;
QComboBox *_announcementColor;
TwitchChannelSelection *_channel;
VariableTextEdit *_chatMessage;
bool _loading = true;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
#pragma once
#include "macro-condition-edit.hpp"
#include "token.hpp"
#include "category-selection.hpp"
#include "channel-selection.hpp"
#include "points-reward-selection.hpp"
#include "chat-connection.hpp"
#include <variable-line-edit.hpp>
#include <variable-text-edit.hpp>
#include <regex-config.hpp>
namespace advss {
class MacroConditionTwitch : public MacroCondition {
public:
MacroConditionTwitch(Macro *m) : MacroCondition(m, true) {}
static std::shared_ptr<MacroCondition> Create(Macro *m)
{
return std::make_shared<MacroConditionTwitch>(m);
}
std::string GetId() const { return id; };
std::string GetShortDesc() const;
enum class Condition {
// Event based
STREAM_ONLINE_EVENT = 0,
STREAM_ONLINE_LIVE_EVENT = 1,
STREAM_ONLINE_PLAYLIST_EVENT = 2,
STREAM_ONLINE_WATCHPARTY_EVENT = 3,
STREAM_ONLINE_PREMIERE_EVENT = 4,
STREAM_ONLINE_RERUN_EVENT = 5,
STREAM_OFFLINE_EVENT = 100,
CHANNEL_INFO_UPDATE_EVENT = 200,
FOLLOW_EVENT = 300,
SUBSCRIPTION_START_EVENT = 400,
SUBSCRIPTION_END_EVENT = 500,
SUBSCRIPTION_GIFT_EVENT = 600,
SUBSCRIPTION_MESSAGE_EVENT = 700,
CHEER_EVENT = 800,
RAID_OUTBOUND_EVENT = 900,
RAID_INBOUND_EVENT = 1000,
SHOUTOUT_OUTBOUND_EVENT = 1100,
SHOUTOUT_INBOUND_EVENT = 1200,
POLL_START_EVENT = 1300,
POLL_PROGRESS_EVENT = 1400,
POLL_END_EVENT = 1500,
PREDICTION_START_EVENT = 1600,
PREDICTION_PROGRESS_EVENT = 1700,
PREDICTION_LOCK_EVENT = 1800,
PREDICTION_END_EVENT = 1900,
GOAL_START_EVENT = 2000,
GOAL_PROGRESS_EVENT = 2100,
GOAL_END_EVENT = 2200,
HYPE_TRAIN_START_EVENT = 2300,
HYPE_TRAIN_PROGRESS_EVENT = 2400,
HYPE_TRAIN_END_EVENT = 2500,
CHARITY_CAMPAIGN_START_EVENT = 2600,
CHARITY_CAMPAIGN_PROGRESS_EVENT = 2700,
CHARITY_CAMPAIGN_DONATION_EVENT = 2800,
CHARITY_CAMPAIGN_END_EVENT = 2900,
SHIELD_MODE_START_EVENT = 3000,
SHIELD_MODE_END_EVENT = 3100,
POINTS_REWARD_ADDITION_EVENT = 3200,
POINTS_REWARD_UPDATE_EVENT = 3300,
POINTS_REWARD_DELETION_EVENT = 3400,
POINTS_REWARD_REDEMPTION_EVENT = 3500,
POINTS_REWARD_REDEMPTION_UPDATE_EVENT = 3600,
USER_BAN_EVENT = 3700,
USER_UNBAN_EVENT = 3800,
USER_MODERATOR_ADDITION_EVENT = 3900,
USER_MODERATOR_DELETION_EVENT = 4000,
// Chat
CHAT_MESSAGE_RECEIVED = 500000,
// Polling
LIVE_POLLING = 1000000,
TITLE_POLLING = 1000100,
CATEGORY_POLLING = 1000200,
};
void SetCondition(const Condition &condition);
Condition GetCondition() const { return _condition; }
void SetChannel(const TwitchChannel &channel);
TwitchChannel GetChannel() const { return _channel; }
void SetPointsReward(const TwitchPointsReward &pointsReward);
TwitchPointsReward GetPointsReward() const { return _pointsReward; }
void ResetChatConnection();
bool CheckCondition();
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
bool ConditionIsSupportedByToken();
std::weak_ptr<TwitchToken> _token;
TwitchChannel _channel;
TwitchPointsReward _pointsReward;
StringVariable _streamTitle = obs_module_text(
"AdvSceneSwitcher.condition.twitch.title.title");
RegexConfig _regexTitle = RegexConfig::PartialMatchRegexConfig();
StringVariable _chatMessage;
RegexConfig _regexChat = RegexConfig::PartialMatchRegexConfig();
TwitchCategory _category;
private:
bool CheckChannelGenericEvents(TwitchToken &token);
bool CheckChannelLiveEvents(TwitchToken &token);
bool CheckChatMessages(TwitchToken &token);
void SetupEventSubscriptions();
void ResetSubscription();
void CheckEventSubscription(EventSub &);
bool IsUsingEventSubCondition();
void AddChannelGenericEventSubscription(
const char *version, bool includeModeratorId = false,
const char *mainUserIdFieldName = "broadcaster_user_id",
obs_data_t *extraConditions = nullptr);
void SetupTempVars();
void SetTempVarValues(const ChannelLiveInfo &);
void SetTempVarValues(const ChannelInfo &);
Condition _condition = Condition::LIVE_POLLING;
std::future<std::string> _subscriptionIDFuture;
std::string _subscriptionID;
std::shared_ptr<TwitchChatConnection> _chatConnection;
static bool _registered;
static const std::string id;
};
class MacroConditionTwitchEdit : public QWidget {
Q_OBJECT
public:
MacroConditionTwitchEdit(
QWidget *parent,
std::shared_ptr<MacroConditionTwitch> entryData = nullptr);
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroCondition> cond)
{
return new MacroConditionTwitchEdit(
parent,
std::dynamic_pointer_cast<MacroConditionTwitch>(cond));
}
void UpdateEntryData();
private slots:
void ConditionChanged(int);
void TwitchTokenChanged(const QString &);
void CheckTokenPermissions();
void ChannelChanged(const TwitchChannel &);
void PointsRewardChanged(const TwitchPointsReward &);
void StreamTitleChanged();
void ChatMessageChanged();
void RegexTitleChanged(RegexConfig);
void RegexChatChanged(RegexConfig);
void CategoreyChanged(const TwitchCategory &);
signals:
void HeaderInfoChanged(const QString &);
void TempVarsChanged();
protected:
std::shared_ptr<MacroConditionTwitch> _entryData;
private:
void SetWidgetVisibility();
QHBoxLayout *_layout;
FilterComboBox *_conditions;
TwitchConnectionSelection *_tokens;
QLabel *_tokenPermissionWarning;
QTimer _tokenPermissionCheckTimer;
TwitchChannelSelection *_channel;
TwitchPointsRewardWidget *_pointsReward;
VariableLineEdit *_streamTitle;
RegexConfigWidget *_regexTitle;
VariableTextEdit *_chatMessage;
RegexConfigWidget *_regexChat;
TwitchCategoryWidget *_category;
bool _loading = true;
};
} // namespace advss

View File

@@ -0,0 +1,219 @@
#include "points-reward-selection.hpp"
#include "twitch-helpers.hpp"
#include <obs-module.h>
#include <utility.hpp>
namespace advss {
void TwitchPointsReward::Load(obs_data_t *obj)
{
OBSDataAutoRelease data = obs_data_get_obj(obj, "pointsReward");
id = obs_data_get_string(data, "id");
title = obs_data_get_string(data, "title");
}
void TwitchPointsReward::Save(obs_data_t *obj) const
{
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "id", id.c_str());
obs_data_set_string(data, "title", title.c_str());
obs_data_set_obj(obj, "pointsReward", data);
}
TwitchPointsRewardSelection::TwitchPointsRewardSelection(QWidget *parent,
bool allowAny)
: FilterComboBox(
parent,
obs_module_text(
"AdvSceneSwitcher.twitch.selection.points.reward.placeholder")),
_allowAny(allowAny)
{
setDuplicatesEnabled(true);
setSizeAdjustPolicy(QComboBox::AdjustToContents);
QWidget::connect(this, SIGNAL(currentIndexChanged(int)), this,
SLOT(SelectionChanged(int)));
}
void TwitchPointsRewardSelection::SetPointsReward(
const TwitchPointsReward &pointsReward)
{
int index = findData(QString::fromStdString(pointsReward.id));
if (index != -1) {
setCurrentIndex(index);
return;
}
setCurrentIndex(-1);
}
void TwitchPointsRewardSelection::SetChannel(const TwitchChannel &channel)
{
_channel = channel;
PopulateSelection();
}
void TwitchPointsRewardSelection::SetToken(
const std::weak_ptr<TwitchToken> &token)
{
_token = token;
if (token.expired()) {
DisplayErrorMessage(obs_module_text(
"AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noAccount"));
return;
}
PopulateSelection();
}
void TwitchPointsRewardSelection::PopulateSelection()
{
auto token = _token.lock();
if (!token || !token->AnyOptionIsEnabled(SUPPORTED_TOKEN_OPTIONS)) {
DisplayErrorMessage(obs_module_text(
"AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noPermission"));
return;
}
if (!_channel || (*_channel).GetName().empty()) {
DisplayErrorMessage(obs_module_text(
"AdvSceneSwitcher.twitch.selection.points.reward.tooltip.noChannel"));
return;
}
auto currentSelection = currentText();
const QSignalBlocker b(this);
clear();
auto pointsRewards = GetPointsRewards(token, *_channel);
if (!pointsRewards) {
DisplayErrorMessage(obs_module_text(
"AdvSceneSwitcher.twitch.selection.points.reward.tooltip.error"));
return;
}
HideErrorMessage();
AddPredefinedItems();
for (const auto &pointsReward : *pointsRewards) {
addItem(QString::fromStdString(pointsReward.title),
QString::fromStdString(pointsReward.id));
}
setCurrentText(currentSelection);
}
void TwitchPointsRewardSelection::AddPredefinedItems()
{
if (_allowAny) {
addItem(obs_module_text(
"AdvSceneSwitcher.twitch.selection.points.reward.option.any"),
"-");
}
}
void TwitchPointsRewardSelection::DisplayErrorMessage(const char *errorMessage)
{
setDisabled(true);
setToolTip(errorMessage);
}
void TwitchPointsRewardSelection::HideErrorMessage()
{
setDisabled(false);
setToolTip("");
}
std::optional<std::vector<TwitchPointsReward>>
TwitchPointsRewardSelection::GetPointsRewards(
const std::shared_ptr<TwitchToken> &token, const TwitchChannel &channel)
{
httplib::Params params = {
{"broadcaster_id", channel.GetUserID(*token)}};
auto response = SendGetRequest(*token, "https://api.twitch.tv",
"/helix/channel_points/custom_rewards",
params);
if (response.status != 200) {
blog(LOG_WARNING,
"Failed to fetch points rewards for user %s and channel %s! (%d)",
token->GetName().c_str(), channel.GetName().c_str(),
response.status);
return {};
}
return ParseResponse(response.data);
}
std::vector<TwitchPointsReward>
TwitchPointsRewardSelection::ParseResponse(obs_data_t *response)
{
std::vector<TwitchPointsReward> pointRewards;
OBSDataArrayAutoRelease jsonArray =
obs_data_get_array(response, "data");
size_t count = obs_data_array_count(jsonArray);
for (size_t i = 0; i < count; ++i) {
OBSDataAutoRelease jsonObj = obs_data_array_item(jsonArray, i);
std::string id = obs_data_get_string(jsonObj, "id");
std::string title = obs_data_get_string(jsonObj, "title");
pointRewards.push_back({id, title});
}
return pointRewards;
}
void TwitchPointsRewardSelection::SelectionChanged(int index)
{
TwitchPointsReward pointsReward{
itemData(index).toString().toStdString(),
currentText().toStdString()};
emit PointsRewardChanged(pointsReward);
}
TwitchPointsRewardWidget::TwitchPointsRewardWidget(QWidget *parent)
: QWidget(parent),
_selection(new TwitchPointsRewardSelection(this)),
_refreshButton(new QPushButton(this))
{
_refreshButton->setMaximumWidth(22);
SetButtonIcon(_refreshButton, ":res/images/refresh.svg");
_refreshButton->setToolTip(obs_module_text(
"AdvSceneSwitcher.twitch.selection.points.reward.refresh"));
auto layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_selection);
layout->addWidget(_refreshButton);
setLayout(layout);
QWidget::connect(
_selection,
SIGNAL(PointsRewardChanged(const TwitchPointsReward &)), this,
SIGNAL(PointsRewardChanged(const TwitchPointsReward &)));
QWidget::connect(_refreshButton, SIGNAL(clicked()), _selection,
SLOT(PopulateSelection()));
}
void TwitchPointsRewardWidget::SetPointsReward(
const TwitchPointsReward &pointsReward)
{
_selection->SetPointsReward(pointsReward);
}
void TwitchPointsRewardWidget::SetChannel(const TwitchChannel &channel)
{
_selection->SetChannel(channel);
}
void TwitchPointsRewardWidget::SetToken(const std::weak_ptr<TwitchToken> &token)
{
_selection->SetToken(token);
}
} // namespace advss

View File

@@ -0,0 +1,74 @@
#pragma once
#include "channel-selection.hpp"
#include "token.hpp"
#include <filter-combo-box.hpp>
#include <obs-data.h>
#include <string>
namespace advss {
struct TwitchPointsReward {
void Load(obs_data_t *obj);
void Save(obs_data_t *obj) const;
std::string id = "-";
std::string title = "-";
};
class TwitchPointsRewardSelection : public FilterComboBox {
Q_OBJECT
public:
TwitchPointsRewardSelection(QWidget *parent, bool allowAny = true);
void SetPointsReward(const TwitchPointsReward &pointsReward);
void SetChannel(const TwitchChannel &channel);
void SetToken(const std::weak_ptr<TwitchToken> &token);
protected:
void DisplayErrorMessage(const char *errorMessage);
void HideErrorMessage();
std::optional<TwitchChannel> _channel = {};
std::weak_ptr<TwitchToken> _token;
private:
void AddPredefinedItems();
std::optional<std::vector<TwitchPointsReward>>
GetPointsRewards(const std::shared_ptr<TwitchToken> &token,
const TwitchChannel &channel);
std::vector<TwitchPointsReward> ParseResponse(obs_data_t *response);
bool _allowAny;
const std::vector<TokenOption> SUPPORTED_TOKEN_OPTIONS = {
{"channel:read:redemptions"},
{"channel:manage:redemptions"}};
private slots:
void PopulateSelection();
void SelectionChanged(int);
signals:
void PointsRewardChanged(const TwitchPointsReward &);
};
class TwitchPointsRewardWidget : public QWidget {
Q_OBJECT
public:
TwitchPointsRewardWidget(QWidget *parent);
void SetPointsReward(const TwitchPointsReward &pointsReward);
void SetChannel(const TwitchChannel &channel);
void SetToken(const std::weak_ptr<TwitchToken> &token);
signals:
void PointsRewardChanged(const TwitchPointsReward &);
protected:
TwitchPointsRewardSelection *_selection;
QPushButton *_refreshButton;
};
} // namespace advss

View File

@@ -1,30 +1,80 @@
#include "token.hpp"
#include "twitch-helpers.hpp"
#include <switcher-data.hpp>
#include <plugin-state-helper.hpp>
#include <log-helper.hpp>
#include <obs-module-helper.hpp>
#include <utility.hpp>
#include <QScrollArea>
#include <QDesktopServices>
#include <QGroupBox>
#include <QUrl>
namespace advss {
static const int tokenGrabberPort = 8080;
static std::deque<std::shared_ptr<Item>> twitchTokens;
const std::unordered_map<std::string, std::string> TokenOption::apiIdToLocale{
// Add necessary token permissions here
/*
{"analytics:read:extensions",
"AdvSceneSwitcher.twitchToken.analytics.readExtensions"},
{"analytics:read:games",
"AdvSceneSwitcher.twitchToken.analytics.readGames"},
{"bits:read", "AdvSceneSwitcher.twitchToken.bits.read"},
*/
const std::unordered_map<std::string, std::string> TokenOption::_apiIdToLocale{
{"channel:manage:broadcast",
"AdvSceneSwitcher.twitchToken.channel.manageBroadcast"},
{"clips:edit", "AdvSceneSwitcher.twitchToken.channel.createClip"},
"AdvSceneSwitcher.twitchToken.channel.broadcast.manage"},
{"moderator:read:followers",
"AdvSceneSwitcher.twitchToken.moderator.followers.read"},
{"channel:read:subscriptions",
"AdvSceneSwitcher.twitchToken.channel.subscriptions.read"},
{"bits:read", "AdvSceneSwitcher.twitchToken.bits.read"},
{"moderator:read:shoutouts",
"AdvSceneSwitcher.twitchToken.moderator.shoutouts.read"},
{"channel:manage:raids",
"AdvSceneSwitcher.twitchToken.channel.raids.manage"},
{"moderator:manage:shoutouts",
"AdvSceneSwitcher.twitchToken.moderator.shoutouts.manage"},
{"channel:read:polls",
"AdvSceneSwitcher.twitchToken.channel.polls.read"},
{"channel:manage:polls",
"AdvSceneSwitcher.twitchToken.channel.polls.manage"},
{"channel:read:predictions",
"AdvSceneSwitcher.twitchToken.channel.predictions.read"},
{"channel:manage:predictions",
"AdvSceneSwitcher.twitchToken.channel.predictions.manage"},
{"channel:read:goals",
"AdvSceneSwitcher.twitchToken.channel.goals.read"},
{"channel:read:hype_train",
"AdvSceneSwitcher.twitchToken.channel.hypeTrain.read"},
{"channel:read:charity",
"AdvSceneSwitcher.twitchToken.channel.charity.read"},
{"moderator:read:shield_mode",
"AdvSceneSwitcher.twitchToken.moderator.shieldMode.read"},
{"moderator:manage:shield_mode",
"AdvSceneSwitcher.twitchToken.moderator.shieldMode.manage"},
{"channel:read:redemptions",
"AdvSceneSwitcher.twitchToken.channel.redemptions.read"},
{"channel:manage:redemptions",
"AdvSceneSwitcher.twitchToken.channel.redemptions.manage"},
{"channel:moderate", "AdvSceneSwitcher.twitchToken.channel.moderate"},
{"moderator:manage:banned_users",
"AdvSceneSwitcher.twitchToken.moderator.bannedUsers.manage"},
{"user:manage:blocked_users",
"AdvSceneSwitcher.twitchToken.user.blockedUsers.manage"},
{"moderation:read", "AdvSceneSwitcher.twitchToken.moderation.read"},
{"channel:manage:moderators",
"AdvSceneSwitcher.twitchToken.channel.moderators.manage"},
{"channel:manage:vips",
"AdvSceneSwitcher.twitchToken.channel.vips.manage"},
{"channel:edit:commercial",
"AdvSceneSwitcher.twitchToken.channel.startCommercial"},
"AdvSceneSwitcher.twitchToken.channel.commercial.edit"},
{"channel:manage:ads",
"AdvSceneSwitcher.twitchToken.channel.ads.manage"},
{"clips:edit", "AdvSceneSwitcher.twitchToken.clips.edit"},
{"moderator:manage:announcements",
"AdvSceneSwitcher.twitchToken.moderator.announcements.manage"},
{"moderator:manage:chat_settings",
"AdvSceneSwitcher.twitchToken.moderator.chat.settings.manage"},
{"user:manage:whispers",
"AdvSceneSwitcher.twitchToken.user.whispers.manage"},
{"chat:read", "AdvSceneSwitcher.twitchToken.chat.read"},
{"chat:edit", "AdvSceneSwitcher.twitchToken.chat.edit"},
};
static void saveConnections(obs_data_t *obj);
@@ -32,8 +82,8 @@ static void loadConnections(obs_data_t *obj);
bool setupTwitchTokenSupport()
{
GetSwitcher()->AddSaveStep(saveConnections);
GetSwitcher()->AddLoadStep(loadConnections);
AddSaveStep(saveConnections);
AddLoadStep(loadConnections);
return true;
}
@@ -78,13 +128,23 @@ void TokenOption::Save(obs_data_t *obj) const
std::string TokenOption::GetLocale() const
{
return apiIdToLocale.at(apiId);
return _apiIdToLocale.at(apiId);
}
const std::unordered_map<std::string, std::string> &
TokenOption::GetTokenOptionMap()
{
return apiIdToLocale;
return _apiIdToLocale;
}
std::set<TokenOption> TokenOption::GetAllTokenOptions()
{
std::set<TokenOption> result;
for (const auto &[optionStr, _] : _apiIdToLocale) {
TokenOption option = {optionStr};
result.emplace(option);
}
return result;
}
bool TokenOption::operator<(const TokenOption &other) const
@@ -132,11 +192,27 @@ bool TwitchToken::OptionIsEnabled(const TokenOption &option) const
return false;
}
bool TwitchToken::AnyOptionIsEnabled(
const std::vector<TokenOption> &options) const
{
if (options.empty()) {
return true;
}
for (const auto &tokenOption : options) {
if (OptionIsEnabled(tokenOption)) {
return true;
}
}
return false;
}
void TwitchToken::SetToken(const std::string &value)
{
_token = value;
auto res =
SendGetRequest("https://api.twitch.tv", "/helix/users", *this);
SendGetRequest(*this, "https://api.twitch.tv", "/helix/users");
if (res.status != 200) {
blog(LOG_WARNING, "failed to get Twitch user id from token!");
_userID = -1;
@@ -150,6 +226,64 @@ void TwitchToken::SetToken(const std::string &value)
_userID = obs_data_get_string(arrayObj, "id");
_name = obs_data_get_string(arrayObj, "display_name");
}
// Trigger resubscribes with new token
if (_eventSub) {
_eventSub->ClearActiveSubscriptions();
}
}
std::optional<std::string> TwitchToken::GetToken() const
{
if (!IsValid()) {
return {};
}
return _token;
}
std::shared_ptr<EventSub> TwitchToken::GetEventSub()
{
if (!_eventSub) {
_eventSub = std::make_shared<EventSub>();
}
return _eventSub;
}
bool TwitchToken::IsValid(bool forceUpdate) const
{
static std::chrono::system_clock::time_point queryTime;
static std::string lastQueryToken;
static httplib::Result response;
static httplib::Client cli("https://id.twitch.tv");
httplib::Headers headers{{"Authorization", "OAuth " + _token}};
auto currentTime = std::chrono::system_clock::now();
auto diff = currentTime - queryTime;
const bool cacheIsTooOld = diff >= std::chrono::hours(1);
const bool tokenChanged = lastQueryToken != _token;
if (tokenChanged) {
response =
cli.Get("/oauth2/validate", httplib::Params{}, headers);
queryTime = std::chrono::system_clock::now();
lastQueryToken = _token;
return response && response->status == 200;
}
// No point in checking again as token will not become valid again
if (!forceUpdate && response && response->status != 200) {
blog(LOG_INFO, "Twitch token %s is not valid!", _name.c_str());
return false;
}
if (!forceUpdate && !cacheIsTooOld && response) {
return response->status == 200;
}
response = cli.Get("/oauth2/validate", httplib::Params{}, headers);
queryTime = std::chrono::system_clock::now();
lastQueryToken = _token;
return response && response->status == 200;
}
TwitchToken *GetTwitchTokenByName(const QString &name)
@@ -283,7 +417,8 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
obs_module_text("AdvSceneSwitcher.twitchToken.request"))),
_showToken(new QPushButton()),
_currentTokenValue(new QLineEdit()),
_tokenStatus(new QLabel())
_tokenStatus(new QLabel()),
_generalSettingsGrid(new QGridLayout())
{
_showToken->setMaximumWidth(22);
_showToken->setFlat(true);
@@ -294,6 +429,7 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
_currentTokenValue->setText(QString::fromStdString(settings._token));
_name->setReadOnly(true);
_showNameEmptyWarning = false;
QWidget::connect(_requestToken, SIGNAL(clicked()), this,
SLOT(RequestToken()));
@@ -304,28 +440,29 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
QWidget::connect(&_tokenGrabber, &TokenGrabberThread::GotToken, this,
&TwitchTokenSettingsDialog::GotToken);
auto generalSettingsGrid = new QGridLayout();
int row = 0;
generalSettingsGrid->addWidget(
_generalSettingsGrid->addWidget(
new QLabel(
obs_module_text("AdvSceneSwitcher.twitchToken.name")),
row, 0);
auto nameLayout = new QHBoxLayout;
auto nameLayout = new QHBoxLayout();
nameLayout->addWidget(_name);
nameLayout->addWidget(_nameHint);
generalSettingsGrid->addLayout(nameLayout, row, 1);
_generalSettingsGrid->addLayout(nameLayout, row, 1);
_nameRow = row;
++row;
generalSettingsGrid->addWidget(
_generalSettingsGrid->addWidget(
new QLabel(
obs_module_text("AdvSceneSwitcher.twitchToken.value")),
row, 0);
auto tokenValueLayout = new QHBoxLayout;
auto tokenValueLayout = new QHBoxLayout();
tokenValueLayout->addWidget(_currentTokenValue);
tokenValueLayout->addWidget(_showToken);
generalSettingsGrid->addLayout(tokenValueLayout, row, 1);
_generalSettingsGrid->addLayout(tokenValueLayout, row, 1);
_tokenValueRow = row;
++row;
generalSettingsGrid->addWidget(_requestToken, row, 0);
generalSettingsGrid->addWidget(_tokenStatus, row, 1);
_generalSettingsGrid->addWidget(_requestToken, row, 0);
_generalSettingsGrid->addWidget(_tokenStatus, row, 1);
auto optionsGrid = new QGridLayout();
row = 0;
@@ -346,7 +483,7 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
auto contentWidget = new QWidget(scrollArea);
auto layout = new QVBoxLayout(contentWidget);
layout->addLayout(generalSettingsGrid);
layout->addLayout(_generalSettingsGrid);
layout->addWidget(optionsBox);
layout->setContentsMargins(0, 0, 0, 0);
scrollArea->setWidget(contentWidget);
@@ -360,6 +497,7 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
if (settings._token.empty()) {
_tokenStatus->setText(obs_module_text(
"AdvSceneSwitcher.twitchToken.request.notSet"));
SetTokenInfoVisible(false);
}
HideToken();
@@ -368,6 +506,29 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
}
_currentToken = settings;
QWidget::connect(&_validationTimer, &QTimer::timeout, this,
&TwitchTokenSettingsDialog::CheckIfTokenValid);
_validationTimer.start(10000);
CheckIfTokenValid();
}
void TwitchTokenSettingsDialog::SetTokenInfoVisible(bool visible)
{
SetGridLayoutRowVisible(_generalSettingsGrid, _nameRow, visible);
SetGridLayoutRowVisible(_generalSettingsGrid, _tokenValueRow, visible);
}
void TwitchTokenSettingsDialog::CheckIfTokenValid()
{
if (_currentToken._token.empty()) {
return;
}
if (_currentToken.IsValid(true)) {
return;
}
_tokenStatus->setText(
obs_module_text("AdvSceneSwitcher.twitchToken.request.notSet"));
}
void TwitchTokenSettingsDialog::ShowToken()
@@ -388,11 +549,32 @@ void TwitchTokenSettingsDialog::TokenOptionChanged(int)
PulseWidget(_requestToken, Qt::green, QColor(0, 0, 0, 0), true);
}
_name->setText("");
SetTokenInfoVisible(false);
QMetaObject::invokeMethod(this, "NameChanged",
Q_ARG(const QString &, ""));
_tokenStatus->setText(
obs_module_text("AdvSceneSwitcher.twitchToken.request.notSet"));
_currentTokenValue->setText("");
}
static void revokeToken(const std::string &token)
{
httplib::Client cli("https://id.twitch.tv");
auto response = cli.Post("/oauth2/revoke",
std::string("client_id=") + GetClientID() +
"&token=" + token,
"application/x-www-form-urlencoded");
if (!response) {
auto err = response.error();
blog(LOG_INFO, "Failed to revoke token: %s",
httplib::to_string(err).c_str());
return;
}
if (response->status != 200) {
blog(LOG_INFO, "Failed to revoke token: %d", response->status);
}
}
static std::string generateStateString()
{
const char *chars =
@@ -429,7 +611,7 @@ static std::string generateScopeString(const std::set<TokenOption> &options)
static std::string getHtml(const QString &redirect)
{
const char *html = R"(
QString html = R"(
<html>
<head>
<title>Advanced scene switcher</title>
@@ -441,17 +623,18 @@ static std::string getHtml(const QString &redirect)
if (document.location.hash && document.location.hash != '') {
var parsedHash = new URLSearchParams(window.location.hash.slice(1));
if (parsedHash.get('access_token')) {
window.location.replace(`http://localhost:8080/token?access_token=${parsedHash.get('access_token')}&state=${parsedHash.get('state')}`);
window.location.replace(`http://localhost:%1/token?access_token=${parsedHash.get('access_token')}&state=${parsedHash.get('state')}`);
output.textContent = 'It is safe to close this window';
}
} else {
window.location.replace('%1');
window.location.replace('%2');
}
</script>
</body>
</html>)";
return QString(html).arg(redirect).toStdString();
return html.arg(QString::number(tokenGrabberPort), redirect)
.toStdString();
}
void TwitchTokenSettingsDialog::RequestToken()
@@ -477,10 +660,14 @@ void TwitchTokenSettingsDialog::GotToken(const std::optional<QString> &value)
auto name = QString::fromStdString(_currentToken._name);
_name->setText(name);
_name->textEdited(name);
QMetaObject::invokeMethod(this, "NameChanged",
Q_ARG(const QString &, name));
SetTokenInfoVisible(true);
} else {
_tokenStatus->setText(obs_module_text(
"AdvSceneSwitcher.twitchToken.request.fail"));
_name->setText("");
SetTokenInfoVisible(false);
}
_requestToken->setEnabled(true);
}
@@ -542,13 +729,16 @@ void TokenGrabberThread::run()
// Generate URI to request token
auto state = generateStateString();
auto getTokenURI = "https://id.twitch.tv/oauth2/authorize"
"?response_type=token"
"&client_id=" +
QString(GetClientID()) +
"&redirect_uri=http://localhost:8080/auth"
"&scope=" +
_scope + "&state=" + QString::fromStdString(state);
QString getTokenURI =
("https://id.twitch.tv/oauth2/authorize"
"?response_type=token"
"&client_id=" +
QString(GetClientID()) +
"&redirect_uri=http://localhost:%1"
"/auth"
"&scope=" +
_scope + "&state=" + QString::fromStdString(state))
.arg(QString::number(tokenGrabberPort));
// Setup server receiving token string
auto html = getHtml(getTokenURI);
@@ -607,8 +797,19 @@ void TokenGrabberThread::run()
// Start the server and wait
std::unique_lock<std::mutex> lock(_mutex);
_serverThread =
std::thread([this]() { _server.listen("localhost", 8080); });
_serverThread = std::thread([this]() {
if (!_server.bind_to_port("localhost", tokenGrabberPort, 0)) {
blog(LOG_WARNING,
"Failed to bind token server to localhost %d!",
tokenGrabberPort);
return;
}
if (!_server.listen_after_bind()) {
blog(LOG_WARNING, "Token server failed to listen()!");
return;
}
vblog(LOG_INFO, "Token server stopped!");
});
auto time = std::chrono::high_resolution_clock::now() +
std::chrono::seconds(_timeout);
while (!_stopWaiting) {

View File

@@ -1,9 +1,13 @@
#pragma once
#include "event-sub.hpp"
#include <item-selection-helpers.hpp>
#include <httplib.h>
#include <set>
#include <QCheckBox>
#include <QThread>
#include <QLayout>
#include <QTimer>
#include <optional>
namespace advss {
@@ -19,11 +23,12 @@ public:
static const std::unordered_map<std::string, std::string> &
GetTokenOptionMap();
static std::set<TokenOption> GetAllTokenOptions();
bool operator<(const TokenOption &other) const;
std::string apiId = "";
private:
const static std::unordered_map<std::string, std::string> apiIdToLocale;
const static std::unordered_map<std::string, std::string> _apiIdToLocale;
};
class TwitchToken : public Item {
@@ -36,16 +41,20 @@ public:
void Load(obs_data_t *obj);
void Save(obs_data_t *obj) const;
std::string GetName() { return _name; }
bool OptionIsEnabled(const TokenOption &) const;
bool OptionIsEnabled(const TokenOption &option) const;
bool AnyOptionIsEnabled(const std::vector<TokenOption> &options) const;
void SetToken(const std::string &);
bool IsEmpty() const { return _token.empty(); }
std::string GetToken() const { return _token; }
std::optional<std::string> GetToken() const;
std::string GetUserID() const { return _userID; }
std::shared_ptr<EventSub> GetEventSub();
bool IsValid(bool forceUpdate = false) const;
private:
std::string _token;
std::string _userID;
std::set<TokenOption> _tokenOptions = {{"channel:manage:broadcast"}};
std::set<TokenOption> _tokenOptions = TokenOption::GetAllTokenOptions();
std::shared_ptr<EventSub> _eventSub;
static bool _setup;
@@ -93,17 +102,23 @@ private slots:
void TokenOptionChanged(int);
void RequestToken();
void GotToken(const std::optional<QString> &);
void CheckIfTokenValid();
private:
std::set<TokenOption> GetEnabledOptions();
void SetTokenInfoVisible(bool);
QPushButton *_requestToken;
QPushButton *_showToken;
QLineEdit *_currentTokenValue;
QLabel *_tokenStatus;
QGridLayout *_generalSettingsGrid;
int _nameRow = -1;
int _tokenValueRow = -1;
TokenGrabberThread _tokenGrabber;
TwitchToken _currentToken;
std::unordered_map<std::string, QCheckBox *> _optionWidgets;
QTimer _validationTimer;
};
class TwitchConnectionSelection : public ItemSelection {

Some files were not shown because too many files have changed in this diff Show More