Compare commits

..

61 Commits

Author SHA1 Message Date
WarmUpTill
21a00d47cc Add "Recording saved" option to Recording condition 2023-11-26 01:44:59 +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
102 changed files with 8264 additions and 667 deletions

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 ]] {

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

@@ -213,6 +213,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
@@ -307,6 +309,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 +319,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 +384,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 ---

View File

@@ -325,7 +325,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 +391,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 +469,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 +575,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 +713,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

@@ -295,6 +295,7 @@ 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.state.saveDone="Recording saved"
AdvSceneSwitcher.condition.record.entry="{{recordState}}"
AdvSceneSwitcher.condition.process="Process"
AdvSceneSwitcher.condition.process.entry="{{processes}}is running{{focused}}and is focused"
@@ -407,7 +408,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 +464,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 +475,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 +492,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 +618,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 +633,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 +651,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 +661,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 +812,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 +826,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}}"
@@ -783,12 +858,23 @@ 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.announcement="Send chat announcement"
AdvSceneSwitcher.action.twitch.type.emoteOnlyEnable="Enable chat's emote-only mode"
AdvSceneSwitcher.action.twitch.type.emoteOnlyDisable="Disable chat's emote-only mode"
AdvSceneSwitcher.action.twitch.type.raid="Raid channel"
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"
; Hotkey
AdvSceneSwitcher.hotkey.startSwitcherHotkey="Start the Advanced Scene Switcher"
@@ -819,6 +905,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 +918,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 +1017,47 @@ 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.commercial.edit="Run commercials on a channel."
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.channel.raid="Manage channel's raids."
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.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.channel.moderation.read="Read channel's moderation data."
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 +1071,378 @@ 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.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.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.selectScene="--select scene--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
AdvSceneSwitcher.selectCurrentScene="Current Scene"
@@ -980,6 +1468,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 +1490,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
@@ -275,7 +275,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 +387,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 +566,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

@@ -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.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

@@ -231,7 +231,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 +317,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

@@ -371,7 +371,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 +436,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 +527,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 +651,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}}"

2
deps/asio vendored

Submodule deps/asio updated: b73dc1d2c0...c465349fa5

1
deps/json vendored Submodule

Submodule deps/json added at bc889afb4c

View File

@@ -325,6 +325,9 @@ void SwitcherData::SetPreconditions()
cursorPosChanged = cursorPos.first != switcher->lastCursorPos.first ||
cursorPos.second != switcher->lastCursorPos.second;
lastCursorPos = GetCursorPos();
// Macro
InvalidateMacroTempVarValues();
}
void ClearWebsocketMessages();

View File

@@ -178,6 +178,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);

View File

@@ -499,6 +499,9 @@ 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);
@@ -529,6 +532,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;

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

@@ -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

@@ -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,125 @@ 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);
_manualSettingValue->setVisible(
_entryData->_action == MacroActionFilter::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionFilter::SettingsInputMethod::
INDIVIDUAL_MANUAL);
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

@@ -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,81 @@ 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);
_manualSettingValue->setVisible(
_entryData->_action == MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::
INDIVIDUAL_MANUAL);
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

@@ -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

@@ -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

@@ -1,4 +1,5 @@
#include "macro-condition-recording.hpp"
#include "macro.hpp"
#include "utility.hpp"
namespace advss {
@@ -10,45 +11,91 @@ 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::SAVE_DONE,
"AdvSceneSwitcher.condition.record.state.saveDone"},
};
MacroConditionRecord::MacroConditionRecord(Macro *m) : MacroCondition(m)
{
OBSOutputAutoRelease output = obs_frontend_get_recording_output();
if (!output) {
return;
}
auto sh = obs_output_get_signal_handler(output);
signal_handler_connect(sh, "stop", StopSignal, this);
}
MacroConditionRecord::~MacroConditionRecord()
{
OBSOutputAutoRelease output = obs_frontend_get_recording_output();
if (!output) {
return;
}
auto sh = obs_output_get_signal_handler(output);
signal_handler_disconnect(sh, "stop", StopSignal, this);
}
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::SAVE_DONE:
if (_recordingSaveDone) {
_recordingSaveDone = false;
return true;
}
return false;
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));
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"));
return true;
}
void MacroConditionRecord::StopSignal(void *c, calldata_t *data)
{
auto condition = static_cast<MacroConditionRecord *>(c);
const auto macro = condition->GetMacro();
if (macro && macro->Paused()) {
return;
}
long long code = 0;
if (!calldata_get_int(data, "code", &code)) {
condition->_recordingSaveDone = false;
}
if (code == OBS_OUTPUT_SUCCESS) {
condition->_recordingSaveDone = true;
}
}
static inline void populateStateSelection(QComboBox *list)
{
for (auto entry : recordStates) {
@@ -89,7 +136,8 @@ void MacroConditionRecordEdit::StateChanged(int value)
}
auto lock = LockContext();
_entryData->_recordState = static_cast<RecordState>(value);
_entryData->_condition =
static_cast<MacroConditionRecord::Condition>(value);
}
void MacroConditionRecordEdit::UpdateEntryData()
@@ -98,8 +146,7 @@ void MacroConditionRecordEdit::UpdateEntryData()
return;
}
_recordState->setCurrentIndex(
static_cast<int>(_entryData->_recordState));
_recordState->setCurrentIndex(static_cast<int>(_entryData->_condition));
}
} // namespace advss

View File

@@ -6,15 +6,10 @@
namespace advss {
enum class RecordState {
STOP,
PAUSE,
START,
};
class MacroConditionRecord : public MacroCondition {
public:
MacroConditionRecord(Macro *m) : MacroCondition(m) {}
MacroConditionRecord(Macro *m);
~MacroConditionRecord();
bool CheckCondition();
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
@@ -24,9 +19,18 @@ public:
return std::make_shared<MacroConditionRecord>(m);
}
RecordState _recordState = RecordState::STOP;
enum class Condition {
STOP,
PAUSE,
START,
SAVE_DONE,
};
Condition _condition = Condition::STOP;
private:
static void StopSignal(void *condition, calldata_t *);
bool _recordingSaveDone = false;
static bool _registered;
static const std::string id;
};

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

@@ -96,6 +96,7 @@ bool MacroConditionWindow::WindowRegexMatches(
void MacroConditionWindow::SetVariableValueBasedOnMatch(
const std::string &matchWindow)
{
SetTempVarValue("window", matchWindow);
if (!IsReferencedInVars()) {
return;
}
@@ -173,6 +174,15 @@ std::string MacroConditionWindow::GetShortDesc() const
return _window;
}
void MacroConditionWindow::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,111 @@ 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
{
TempVariable *result = nullptr;
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

@@ -244,6 +244,7 @@ void AdvSceneSwitcher::ExportMacros()
obs_data_release(obj);
}
obs_data_set_array(data, "macros", macroArray);
switcher->SaveVariables(data);
obs_data_array_release(macroArray);
obs_data_set_string(data, "version", g_GIT_TAG);
auto json = obs_data_get_json(data);
@@ -367,6 +368,26 @@ 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;
@@ -380,6 +401,7 @@ void AdvSceneSwitcher::ImportMacros()
ImportMacros();
return;
}
importVariables(data);
auto version = obs_data_get_string(data, "version");
if (strcmp(version, g_GIT_TAG) != 0) {

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

@@ -276,9 +276,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 +361,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 +734,9 @@ bool Macro::PostLoad()
for (auto &a : _actions) {
a->PostLoad();
}
for (auto &a : _elseActions) {
a->PostLoad();
}
return true;
}
@@ -1193,4 +1320,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

@@ -920,7 +920,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;
}
@@ -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("https://api.twitch.tv", "/helix/users",
token, {{"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("https://api.twitch.tv", "/helix/streams",
token, 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("https://api.twitch.tv", "/helix/channels",
token, 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,716 @@
#include "chat-connection.hpp"
#include "token.hpp"
#include "twitch-helpers.hpp"
#include <log-helper.hpp>
#include <switcher-data.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()
{
GetSwitcher()->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;
EventSubWSClient::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,
EventSubWSClient::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)
{
EventSubWSClient::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)
{
EventSubWSClient::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,439 @@
#include "event-sub.hpp"
#include "token.hpp"
#include "twitch-helpers.hpp"
#include <log-helper.hpp>
#include <switcher-data.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()
{
GetSwitcher()->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(registerSubscriptionURL.data(),
registerSubscriptionPath.data(), *token,
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,6 +13,11 @@ 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"},
@@ -24,6 +29,39 @@ const static std::map<MacroActionTwitch::Action, std::string> actionTypes = {
"AdvSceneSwitcher.action.twitch.type.clip"},
{MacroActionTwitch::Action::COMMERCIAL,
"AdvSceneSwitcher.action.twitch.type.commercial"},
{MacroActionTwitch::Action::ANNOUNCEMENT,
"AdvSceneSwitcher.action.twitch.type.announcement"},
{MacroActionTwitch::Action::ENABLE_EMOTE_ONLY,
"AdvSceneSwitcher.action.twitch.type.emoteOnlyEnable"},
{MacroActionTwitch::Action::DISABLE_EMOTE_ONLY,
"AdvSceneSwitcher.action.twitch.type.emoteOnlyDisable"},
{MacroActionTwitch::Action::RAID,
"AdvSceneSwitcher.action.twitch.type.raid"},
{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"},
};
void MacroActionTwitch::SetStreamTitle(
@@ -93,6 +131,7 @@ void MacroActionTwitch::CreateStreamClip(
{
OBSDataAutoRelease data = obs_data_create();
auto hasDelay = _clipHasDelay ? "true" : "false";
auto result = SendPostRequest(
"https://api.twitch.tv",
"/helix/clips?broadcaster_id=" + token->GetUserID() +
@@ -120,15 +159,82 @@ 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("https://api.twitch.tv",
"/helix/chat/announcements?broadcaster_id=" +
userId + "&moderator_id=" + userId,
*token, 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("https://api.twitch.tv",
"/helix/chat/settings?broadcaster_id=" +
userId + "&moderator_id=" + userId,
*token, 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("https://api.twitch.tv", "/helix/raids",
*token, 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();
@@ -152,6 +258,21 @@ bool MacroActionTwitch::PerformAction()
case MacroActionTwitch::Action::COMMERCIAL:
StartCommercial(token);
break;
case MacroActionTwitch::Action::ANNOUNCEMENT:
SendChatAnnouncement(token);
break;
case MacroActionTwitch::Action::ENABLE_EMOTE_ONLY:
SetChatEmoteOnlyMode(token, true);
break;
case MacroActionTwitch::Action::DISABLE_EMOTE_ONLY:
SetChatEmoteOnlyMode(token, false);
break;
case MacroActionTwitch::Action::RAID:
StartRaid(token);
break;
case MacroActionTwitch::Action::SEND_CHAT_MESSAGE:
SendChatMessage(token);
break;
default:
break;
}
@@ -175,6 +296,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 +305,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,12 +325,14 @@ 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()
@@ -212,6 +343,12 @@ bool MacroActionTwitch::ActionIsSupportedByToken()
{Action::MARKER, {"channel:manage:broadcast"}},
{Action::CLIP, {"clips:edit"}},
{Action::COMMERCIAL, {"channel:edit:commercial"}},
{Action::ANNOUNCEMENT, {"moderator:manage:announcements"}},
{Action::ENABLE_EMOTE_ONLY, {"moderator:manage:chat_settings"}},
{Action::DISABLE_EMOTE_ONLY,
{"moderator:manage:chat_settings"}},
{Action::RAID, {"channel:manage:raids"}},
{Action::SEND_CHAT_MESSAGE, {"chat:edit"}},
};
auto token = _token.lock();
if (!token) {
@@ -219,12 +356,28 @@ bool MacroActionTwitch::ActionIsSupportedByToken()
}
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,18 +385,21 @@ 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);
@@ -251,16 +407,23 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
_markerDescription->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_markerDescription->setMaxLength(140);
_announcementMessage->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_announcementMessage->setMaxLength(500);
auto spinBox = _duration->SpinBox();
spinBox->setSuffix("s");
spinBox->setMaximum(180);
populateActionSelection(_actions);
populateAnnouncementColorSelection(_announcementColor);
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,
@@ -269,36 +432,55 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
QWidget::connect(_markerDescription, SIGNAL(editingFinished()), this,
SLOT(MarkerDescriptionChanged()));
QObject::connect(_clipHasDelay, SIGNAL(stateChanged(int)), this,
SLOT(HasClipDelayChanged(const Duration &)));
SLOT(ClipHasDelayChanged(int)));
QObject::connect(_duration, SIGNAL(DurationChanged(const Duration &)),
this, SLOT(DurationChanged(const Duration &)));
QWidget::connect(&_tokenPermissionCheckTimer, SIGNAL(timeout()), this,
SLOT(CheckTokenPermissions()));
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()));
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());
SetupWidgetVisibility();
SetWidgetLayout();
}
void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token)
{
if (_loading || !_entryData) {
@@ -308,11 +490,20 @@ void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token)
auto lock = LockContext();
_entryData->_token = GetWeakTwitchTokenByQString(token);
_category->SetToken(_entryData->_token);
_manualCategorySearch->SetToken(_entryData->_token);
_channel->SetToken(_entryData->_token);
SetupWidgetVisibility();
emit(HeaderInfoChanged(token));
}
void MacroActionTwitchEdit::CheckTokenPermissions()
{
_tokenPermissionWarning->setVisible(
_entryData && !_entryData->ActionIsSupportedByToken());
adjustSize();
updateGeometry();
}
void MacroActionTwitchEdit::StreamTitleChanged()
{
if (_loading || !_entryData) {
@@ -364,29 +555,53 @@ 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::AnnouncementColorChanged(int index)
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_announcementColor =
static_cast<MacroActionTwitch::AnnouncementColor>(index);
}
void MacroActionTwitchEdit::SetupWidgetVisibility()
{
_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);
_announcementMessage->setVisible(
_entryData->_action == MacroActionTwitch::Action::ANNOUNCEMENT);
_announcementColor->setVisible(_entryData->_action ==
MacroActionTwitch::Action::ANNOUNCEMENT);
_channel->setVisible(
_entryData->_action == MacroActionTwitch::Action::RAID ||
_entryData->_action ==
MacroActionTwitch::Action::SEND_CHAT_MESSAGE);
_chatMessage->setVisible(_entryData->_action ==
MacroActionTwitch::Action::SEND_CHAT_MESSAGE);
if (_entryData->_action == MacroActionTwitch::Action::TITLE ||
_entryData->_action == MacroActionTwitch::Action::MARKER) {
RemoveStretchIfPresent(_layout);
@@ -401,32 +616,82 @@ 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);
_entryData->_channel = channel;
}
void MacroActionTwitchEdit::ChatMessageChanged()
{
if (_loading || !_entryData) {
return;
}
auto lock = LockContext();
_entryData->_chatMessage = _chatMessage->toPlainText().toStdString();
adjustSize();
updateGeometry();
}
void MacroActionTwitchEdit::SetWidgetLayout()
{
static 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);
SetupWidgetVisibility();
}

View File

@@ -2,8 +2,11 @@
#include "macro-action-edit.hpp"
#include "token.hpp"
#include "category-selection.hpp"
#include "channel-selection.hpp"
#include "chat-connection.hpp"
#include <variable-line-edit.hpp>
#include <variable-text-edit.hpp>
#include <duration-control.hpp>
namespace advss {
@@ -11,26 +14,41 @@ 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,
CATEGORY = 10,
MARKER = 20,
CLIP = 30,
COMMERCIAL = 40,
ANNOUNCEMENT = 50,
ENABLE_EMOTE_ONLY = 60,
DISABLE_EMOTE_ONLY = 70,
RAID = 80,
SEND_CHAT_MESSAGE = 90,
};
enum class AnnouncementColor {
PRIMARY,
BLUE,
GREEN,
ORANGE,
PURPLE,
};
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::TITLE;
std::weak_ptr<TwitchToken> _token;
StringVariable _streamTitle =
@@ -40,6 +58,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 +70,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 +89,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 +96,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 &);
@@ -86,18 +120,23 @@ protected:
private:
void SetupWidgetVisibility();
void SetWidgetLayout();
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,191 @@
#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 {
// Generic event based
STREAM_ONLINE_EVENT = 0,
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,
// Event based with custom logic
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,
// 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 SetupWidgetVisibility();
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("https://api.twitch.tv",
"/helix/channel_points/custom_rewards",
*token, 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

@@ -10,21 +10,53 @@ namespace advss {
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"},
{"clips:edit", "AdvSceneSwitcher.twitchToken.clips.edit"},
{"channel:edit:commercial",
"AdvSceneSwitcher.twitchToken.channel.startCommercial"},
"AdvSceneSwitcher.twitchToken.channel.commercial.edit"},
{"moderator:manage:announcements",
"AdvSceneSwitcher.twitchToken.moderator.announcements.manage"},
{"moderator:manage:chat_settings",
"AdvSceneSwitcher.twitchToken.moderator.chat.settings.manage"},
{"channel:manage:raids", "AdvSceneSwitcher.twitchToken.channel.raid"},
{"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"},
{"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"},
{"moderation:read",
"AdvSceneSwitcher.twitchToken.channel.moderation.read"},
{"chat:read", "AdvSceneSwitcher.twitchToken.chat.read"},
{"chat:edit", "AdvSceneSwitcher.twitchToken.chat.edit"},
};
static void saveConnections(obs_data_t *obj);
@@ -78,13 +110,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,6 +174,22 @@ 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;
@@ -150,6 +208,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 +399,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 +411,7 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
_currentTokenValue->setText(QString::fromStdString(settings._token));
_name->setReadOnly(true);
_showNameEmptyWarning = false;
QWidget::connect(_requestToken, SIGNAL(clicked()), this,
SLOT(RequestToken()));
@@ -304,28 +422,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 +465,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 +479,7 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
if (settings._token.empty()) {
_tokenStatus->setText(obs_module_text(
"AdvSceneSwitcher.twitchToken.request.notSet"));
SetTokenInfoVisible(false);
}
HideToken();
@@ -368,6 +488,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 +531,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 =
@@ -477,10 +641,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);
}

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 {

View File

@@ -4,11 +4,63 @@
namespace advss {
static constexpr std::string_view clientID = "ds5tt4ogliifsqc04mz3d3etnck3e5";
static const int cacheTimeoutSeconds = 10;
static httplib::Headers getTokenRequestHeaders(const TwitchToken &token)
class Args {
public:
Args(const std::string &uri, const std::string &path,
const std::string &data, httplib::Params params,
httplib::Headers headers)
: _uri(uri),
_path(path),
_data(data),
_params(params),
_headers(headers)
{
}
bool operator<(const Args &other) const
{
bool ret = true;
ret = ret && _uri < other._uri;
ret = ret && _path < other._path;
ret = ret && _params < other._params;
ret = ret && _data < other._data;
ret = ret && _headers < other._headers;
return ret;
}
private:
std::string _uri;
std::string _path;
std::string _data;
httplib::Params _params;
httplib::Headers _headers;
};
struct CacheEntry {
RequestResult result;
std::chrono::system_clock::time_point cacheTime =
std::chrono::system_clock::now();
};
static bool chacheIsTooOld(const CacheEntry &cache)
{
auto currentTime = std::chrono::system_clock::now();
auto diff = currentTime - cache.cacheTime;
return diff >= std::chrono::seconds(cacheTimeoutSeconds);
}
static bool cacheIsValid(const std::map<Args, CacheEntry> &cache,
const Args &args)
{
auto it = cache.find(args);
return it != cache.end() && !chacheIsTooOld(it->second);
}
static httplib::Headers getTokenRequestHeaders(const std::string &token)
{
return {
{"Authorization", "Bearer " + token.GetToken()},
{"Authorization", "Bearer " + token},
{"Client-Id", clientID.data()},
};
}
@@ -18,7 +70,11 @@ RequestResult SendGetRequest(const std::string &uri, const std::string &path,
const httplib::Params &params)
{
httplib::Client cli(uri);
auto headers = getTokenRequestHeaders(token);
auto tokenStr = token.GetToken();
if (!tokenStr) {
return {};
}
auto headers = getTokenRequestHeaders(*tokenStr);
auto response = cli.Get(path, params, headers);
if (!response) {
auto err = response.error();
@@ -37,11 +93,38 @@ RequestResult SendGetRequest(const std::string &uri, const std::string &path,
return result;
}
RequestResult SendGetRequest(const std::string &uri, const std::string &path,
const TwitchToken &token,
const httplib::Params &params, bool useCache)
{
static std::map<Args, CacheEntry> cache;
static std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx);
auto tokenStr = token.GetToken();
if (!tokenStr) {
return {};
}
auto headers = getTokenRequestHeaders(*tokenStr);
Args args(uri, path, "", params, headers);
if (useCache && cacheIsValid(cache, args)) {
auto it = cache.find(args);
return it->second.result;
}
auto result = SendGetRequest(uri, path, token, params);
cache[args] = {result};
return result;
}
RequestResult SendPostRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data)
{
httplib::Client cli(uri);
auto headers = getTokenRequestHeaders(token);
auto tokenStr = token.GetToken();
if (!tokenStr) {
return {};
}
auto headers = getTokenRequestHeaders(*tokenStr);
auto json = obs_data_get_json(data);
std::string body = json ? json : "";
auto response = cli.Post(path, headers, body, "application/json");
@@ -62,11 +145,39 @@ RequestResult SendPostRequest(const std::string &uri, const std::string &path,
return result;
}
RequestResult SendPostRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data,
bool useCache)
{
static std::map<Args, CacheEntry> cache;
static std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx);
auto tokenStr = token.GetToken();
if (!tokenStr) {
return {};
}
auto headers = getTokenRequestHeaders(*tokenStr);
auto jsonCstr = obs_data_get_json(data);
Args args(uri, path, jsonCstr ? jsonCstr : "", {}, headers);
if (useCache && cacheIsValid(cache, args)) {
auto it = cache.find(args);
return it->second.result;
}
auto result = SendPostRequest(uri, path, token, data);
cache[args] = {result};
return result;
}
RequestResult SendPatchRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data)
{
httplib::Client cli(uri);
auto headers = getTokenRequestHeaders(token);
auto tokenStr = token.GetToken();
if (!tokenStr) {
return {};
}
auto headers = getTokenRequestHeaders(*tokenStr);
auto json = obs_data_get_json(data);
std::string body = json ? json : "";
auto response = cli.Patch(path, headers, body, "application/json");
@@ -87,6 +198,30 @@ RequestResult SendPatchRequest(const std::string &uri, const std::string &path,
return result;
}
RequestResult SendPatchRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data,
bool useCache)
{
static std::map<Args, CacheEntry> cache;
static std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx);
auto tokenStr = token.GetToken();
if (!tokenStr) {
return {};
}
auto headers = getTokenRequestHeaders(*tokenStr);
auto jsonCstr = obs_data_get_json(data);
Args args(uri, path, jsonCstr ? jsonCstr : "", {}, headers);
if (useCache && cacheIsValid(cache, args)) {
auto it = cache.find(args);
return it->second.result;
}
auto result = SendPatchRequest(uri, path, token, data);
cache[args] = {result};
return result;
}
const char *GetClientID()
{
return clientID.data();

View File

@@ -2,6 +2,7 @@
#include <httplib.h>
#include <obs.hpp>
#include <string>
#include <chrono>
namespace advss {
@@ -12,6 +13,8 @@ struct RequestResult {
OBSData data = nullptr;
};
// These functions do *not* use or create RequestResult cache entries
RequestResult SendGetRequest(const std::string &uri, const std::string &path,
const TwitchToken &token,
const httplib::Params & = {});
@@ -19,6 +22,20 @@ RequestResult SendPostRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data);
RequestResult SendPatchRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data);
// These functions will cache the RequestResult for 10s
// Note that the cache will be reported as a "memory leak" on OBS shutdown
RequestResult SendGetRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const httplib::Params &,
bool useCache);
RequestResult SendPostRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data,
bool useCache);
RequestResult SendPatchRequest(const std::string &uri, const std::string &path,
const TwitchToken &token, const OBSData &data,
bool useCache);
const char *GetClientID();
} // namespace advss

View File

@@ -178,6 +178,9 @@ cv::Mat PreprocessForOCR(const QImage &image, const QColor &textColor,
std::string RunOCR(tesseract::TessBaseAPI *ocr, const QImage &image,
const QColor &color, double colorDiff)
{
(void)ocr;
(void)color;
(void)colorDiff;
if (image.isNull()) {
return "";
}

View File

@@ -112,7 +112,7 @@ void PreviewDialog::Stop()
_thread.wait();
}
void PreviewDialog::closeEvent(QCloseEvent *event)
void PreviewDialog::closeEvent(QCloseEvent *)
{
Stop();
}

View File

@@ -186,4 +186,9 @@ void SwitcherData::AddLoadStep(std::function<void(obs_data_t *)> function)
loadSteps.emplace_back(function);
}
void SwitcherData::AddPostLoadStep(std::function<void()> function)
{
postLoadSteps.emplace_back(function);
}
} // namespace advss

View File

@@ -66,6 +66,7 @@ public:
void ResetForNextInterval();
void AddSaveStep(std::function<void(obs_data_t *)>);
void AddLoadStep(std::function<void(obs_data_t *)>);
void AddPostLoadStep(std::function<void()>);
void AddIntervalResetStep(std::function<void()>);
bool CheckForMatch(OBSWeakSource &scene, OBSWeakSource &transition,
int &linger, bool &setPreviousSceneAsMatch,
@@ -116,6 +117,7 @@ public:
std::vector<std::function<void(obs_data_t *)>> saveSteps;
std::vector<std::function<void(obs_data_t *)>> loadSteps;
std::vector<std::function<void()>> postLoadSteps;
std::vector<std::function<void()>> resetIntervalSteps;
bool firstBoot = true;

View File

@@ -65,31 +65,56 @@ void FilterSelection::LoadFallback(obs_data_t *obj,
_filterName = obs_data_get_string(obj, name);
}
OBSWeakSource FilterSelection::GetFilter(const SourceSelection &source) const
static std::vector<OBSWeakSource> getFiltersOfSource(OBSWeakSource source)
{
if (!source) {
return {};
}
auto enumFilters = [](obs_source_t *, obs_source_t *filter, void *ptr) {
auto filters =
reinterpret_cast<std::vector<OBSWeakSource> *>(ptr);
OBSWeakSourceAutoRelease weakFilter =
obs_source_get_weak_source(filter);
filters->emplace_back(weakFilter);
};
std::vector<OBSWeakSource> filters;
OBSSourceAutoRelease s = obs_weak_source_get_source(source);
obs_source_enum_filters(s, enumFilters, &filters);
return filters;
}
std::vector<OBSWeakSource>
FilterSelection::GetFilters(const SourceSelection &source) const
{
switch (_type) {
case Type::ALL:
return getFiltersOfSource(source.GetSource());
case Type::SOURCE:
return GetWeakFilterByName(
return {GetWeakFilterByName(
source.GetSource(),
_filter ? GetWeakSourceName(_filter).c_str()
: _filterName.c_str());
: _filterName.c_str())};
case Type::VARIABLE: {
auto var = _variable.lock();
if (!var) {
return nullptr;
return {};
}
return GetWeakFilterByName(source.GetSource(),
var->Value().c_str());
return {GetWeakFilterByName(source.GetSource(),
var->Value().c_str())};
}
default:
break;
}
return nullptr;
return {};
}
std::string FilterSelection::ToString(bool resolve) const
{
switch (_type) {
case Type::ALL:
return obs_module_text("AdvSceneSwitcher.filterSelection.all");
case Type::SOURCE:
return _filter ? GetWeakSourceName(_filter) : _filterName;
case Type::VARIABLE: {
@@ -117,7 +142,9 @@ FilterSelection FilterSelectionWidget::CurrentSelection()
return s;
}
if (idx < _variablesEndIdx) {
if (idx < _allEndIdx) {
s._type = FilterSelection::Type::ALL;
} else if (idx < _variablesEndIdx) {
s._type = FilterSelection::Type::VARIABLE;
s._variable = GetWeakVariableByQString(name);
} else if (idx < _filterEndIdx) {
@@ -139,6 +166,12 @@ void FilterSelectionWidget::PopulateSelection()
{
const QSignalBlocker b(this);
clear();
AddSelectionGroup(
this,
{obs_module_text("AdvSceneSwitcher.filterSelection.all")});
_allEndIdx = count();
if (_addVariables) {
const QStringList variables = GetVariablesNameList();
AddSelectionGroup(this, variables);
@@ -188,6 +221,10 @@ void FilterSelectionWidget::SetFilter(const SourceSelection &source,
int idx = -1;
switch (filter.GetType()) {
case FilterSelection::Type::ALL:
idx = findText(obs_module_text(
"AdvSceneSwitcher.filterSelection.all"));
break;
case FilterSelection::Type::SOURCE: {
if (_filterEndIdx == -1) {
idx = -1;

View File

@@ -13,15 +13,17 @@ public:
enum class Type {
SOURCE,
VARIABLE,
ALL,
};
Type GetType() const { return _type; }
OBSWeakSource GetFilter(const SourceSelection &source) const;
std::vector<OBSWeakSource>
GetFilters(const SourceSelection &source) const;
std::string ToString(bool resolve = false) const;
private:
// TODO: Remove in future version
// Used for backwards compatability to older settings versions
// Used for backwards compatibility to older settings versions
void LoadFallback(obs_data_t *obj, const SourceSelection &source,
const char *name);
@@ -65,9 +67,11 @@ private:
// Order of entries
// 1. "select entry" entry
// 2. Variables
// 3. Regular filters
// 2. All filters
// 3. Variables
// 4. Regular filters
const int _selectIdx = 0;
int _allEndIdx = -1;
int _variablesEndIdx = -1;
int _filterEndIdx = -1;
};

View File

@@ -298,8 +298,14 @@ void ItemSettingsDialog::NameChanged(const QString &text)
return;
}
if (text.isEmpty()) {
SetNameWarning(
obs_module_text("AdvSceneSwitcher.item.emptyName"));
if (_showNameEmptyWarning) {
SetNameWarning(obs_module_text(
"AdvSceneSwitcher.item.emptyName"));
return;
}
_nameHint->setText("");
_nameHint->hide();
_buttonbox->button(QDialogButtonBox::Ok)->setDisabled(true);
return;
}
if (text == obs_module_text(_selectStr.data()) ||

View File

@@ -59,6 +59,7 @@ protected:
std::string_view _addStr;
std::string_view _conflictStr;
QString _originalName;
bool _showNameEmptyWarning = true;
};
typedef bool (*SettingsCallback)(QWidget *, Item &);

View File

@@ -1,6 +1,7 @@
#include "macro-export-import-dialog.hpp"
#include "obs-module-helper.hpp"
#include <obs.hpp>
#include <QLayout>
#include <QLabel>
#include <QDialogButtonBox>
@@ -37,22 +38,49 @@ MacroExportImportDialog::MacroExportImportDialog(Type type)
setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
}
QString compressMacroString(const QString &input)
{
QByteArray inputData = input.toUtf8();
auto compressedData = qCompress(inputData);
QByteArray encodedData = compressedData.toBase64();
return QString::fromUtf8(encodedData);
}
QString decompressMacroString(const QString &input)
{
QByteArray encodedData = input.toUtf8();
QByteArray compressedData = QByteArray::fromBase64(encodedData);
auto outputData = qUncompress(compressedData);
return QString::fromUtf8(outputData);
}
void MacroExportImportDialog::ExportMacros(const QString &json)
{
MacroExportImportDialog dialog(
MacroExportImportDialog::Type::EXPORT_MACRO);
dialog._importExportString->setPlainText(json);
dialog._importExportString->setPlainText(compressMacroString(json));
dialog.adjustSize();
dialog.updateGeometry();
dialog.exec();
}
static bool isValidData(const QString &json)
{
OBSDataAutoRelease data =
obs_data_create_from_json(json.toStdString().c_str());
return !!data;
}
bool MacroExportImportDialog::ImportMacros(QString &json)
{
MacroExportImportDialog dialog(
MacroExportImportDialog::Type::IMPORT_MACRO);
if (dialog.exec() == QDialog::Accepted) {
json = dialog._importExportString->toPlainText();
json = decompressMacroString(
dialog._importExportString->toPlainText());
if (!isValidData(json)) { // Fallback to support raw json format
json = dialog._importExportString->toPlainText();
}
return true;
}
return false;

View File

@@ -1,5 +1,8 @@
#include "resizing-text-edit.hpp"
#include <obs-module.h>
#include <utility.hpp>
namespace advss {
ResizingPlainTextEdit::ResizingPlainTextEdit(QWidget *parent,
@@ -13,6 +16,18 @@ ResizingPlainTextEdit::ResizingPlainTextEdit(QWidget *parent,
{
QWidget::connect(this, SIGNAL(textChanged()), this,
SLOT(ResizeTexteditArea()));
QWidget::connect(this, SIGNAL(textChanged()), this,
SLOT(PreventExceedingMaxLength()));
}
int ResizingPlainTextEdit::maxLength()
{
return _maxLength;
}
void ResizingPlainTextEdit::setMaxLength(int maxLength)
{
_maxLength = maxLength;
}
void ResizingPlainTextEdit::ResizeTexteditArea()
@@ -32,4 +47,19 @@ void ResizingPlainTextEdit::ResizeTexteditArea()
updateGeometry();
}
void ResizingPlainTextEdit::PreventExceedingMaxLength()
{
auto plainText = QPlainTextEdit::toPlainText();
if (_maxLength > -1 && plainText.length() > _maxLength) {
QPlainTextEdit::setPlainText(plainText.left(_maxLength));
QPlainTextEdit::moveCursor(QTextCursor::End);
DisplayMessage(
QString(obs_module_text(
"AdvSceneSwitcher.validation.text.maxLength"))
.arg(QString::number(_maxLength)));
}
}
} // namespace advss

View File

@@ -10,13 +10,18 @@ public:
const int minLines = 3,
const int paddingLines = 2);
virtual ~ResizingPlainTextEdit(){};
int maxLength();
void setMaxLength(int maxLength);
private slots:
void ResizeTexteditArea();
void PreventExceedingMaxLength();
private:
const int _scrollAt;
const int _minLines;
const int _paddingLines;
int _maxLength = -1;
};
} // namespace advss

View File

@@ -24,7 +24,7 @@ public:
private:
// TODO: Remove in future version
// Used for backwards compatability to older settings versions
// Used for backwards compatibility to older settings versions
void LoadFallback(obs_data_t *obj, const char *name);
OBSWeakSource _source;

View File

@@ -0,0 +1,229 @@
#include "source-setting.hpp"
#include "obs-module-helper.hpp"
#include "math-helpers.hpp"
#include "utility.hpp"
#include <nlohmann/json.hpp>
Q_DECLARE_METATYPE(advss::SourceSetting);
namespace advss {
SourceSetting::SourceSetting(const std::string &id,
const std::string &description,
const std::string &longDescription)
: _id(id), _description(description), _longDescription(longDescription)
{
}
bool SourceSetting::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, "description", _description.c_str());
obs_data_set_obj(obj, "sourceSetting", data);
return true;
}
bool SourceSetting::Load(obs_data_t *obj)
{
OBSDataAutoRelease data = obs_data_get_obj(obj, "sourceSetting");
_id = obs_data_get_string(data, "id");
_description = obs_data_get_string(data, "description");
return true;
}
bool SourceSetting::operator==(const SourceSetting &other) const
{
return _id == other._id;
}
std::vector<SourceSetting> GetSoruceSettings(obs_source_t *source)
{
auto properties = obs_source_properties(source);
if (!properties) {
return {};
}
std::vector<SourceSetting> settings;
auto it = obs_properties_first(properties);
do {
if (!it) {
continue;
}
SourceSetting setting(
obs_property_name(it), obs_property_description(it),
obs_property_long_description(it)
? obs_property_long_description(it)
: "");
settings.emplace_back(setting);
} while (obs_property_next(&it));
obs_properties_destroy(properties);
return settings;
}
std::string GetSourceSettingValue(const OBSWeakSource &ws,
const SourceSetting &setting)
{
OBSSourceAutoRelease source = obs_weak_source_get_source(ws);
OBSDataAutoRelease data = obs_source_get_settings(source);
if (!data) {
return "";
}
OBSDataAutoRelease dataWithDefaults = obs_data_get_defaults(data);
obs_data_apply(dataWithDefaults, data);
auto json = obs_data_get_json(dataWithDefaults);
if (!json) {
return "";
}
auto value = GetJsonField(json, setting.GetID());
return value.value_or("");
}
void SetSourceSetting(obs_source_t *source, const SourceSetting &setting,
const std::string &value)
{
auto id = setting.GetID();
OBSDataAutoRelease data = obs_source_get_settings(source);
auto item = obs_data_item_byname(data, id.c_str());
auto type = obs_data_item_gettype(item);
switch (type) {
case OBS_DATA_NULL:
break;
case OBS_DATA_STRING:
obs_data_set_string(data, id.c_str(), value.c_str());
break;
case OBS_DATA_NUMBER: {
auto type = obs_data_item_numtype(item);
switch (type) {
case OBS_DATA_NUM_INVALID:
break;
case OBS_DATA_NUM_INT: {
auto intValue = GetInt(value);
if (intValue.has_value()) {
obs_data_set_int(data, id.c_str(), *intValue);
break;
}
auto doubleValue = GetDouble(value);
if (doubleValue.has_value()) {
obs_data_set_int(data, id.c_str(),
*doubleValue);
}
break;
}
case OBS_DATA_NUM_DOUBLE: {
auto numValue = GetDouble(value);
if (!numValue.has_value()) {
break;
}
obs_data_set_int(data, id.c_str(), *numValue);
break;
}
}
break;
}
case OBS_DATA_BOOLEAN:
obs_data_set_bool(data, id.c_str(), value == "true");
break;
case OBS_DATA_OBJECT: {
OBSDataAutoRelease json =
obs_data_create_from_json(value.c_str());
obs_data_set_obj(data, id.c_str(), json);
break;
}
case OBS_DATA_ARRAY: {
auto jsonStr = obs_data_get_json(data);
if (!jsonStr) {
break;
}
std::string resultJsonStr;
try {
auto json = nlohmann::json::parse(jsonStr);
auto jsonArray = nlohmann::json::parse(value);
json[id] = jsonArray;
resultJsonStr = json.dump();
} catch (const nlohmann::json::exception &) {
break;
}
OBSDataAutoRelease newData =
obs_data_create_from_json(resultJsonStr.c_str());
if (!newData) {
break;
}
obs_data_clear(data);
obs_data_apply(data, newData);
}
}
obs_data_item_release(&item);
obs_source_update(source, data);
}
SourceSettingSelection::SourceSettingSelection(QWidget *parent)
: QWidget(parent),
_settings(new FilterComboBox(
this, obs_module_text("AdvSceneSwitcher.selectSetting"))),
_tooltip(new QLabel())
{
QString path = GetThemeTypeName() == "Light"
? ":/res/images/help.svg"
: ":/res/images/help_light.svg";
QIcon icon(path);
QPixmap pixmap = icon.pixmap(QSize(16, 16));
_tooltip->setPixmap(pixmap);
_tooltip->hide();
QWidget::connect(_settings, SIGNAL(currentIndexChanged(int)), this,
SLOT(SelectionIdxChanged(int)));
auto layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_settings);
layout->addWidget(_tooltip);
setLayout(layout);
}
void SourceSettingSelection::SetSource(const OBSWeakSource &source)
{
_settings->clear();
Populate(source);
}
void SourceSettingSelection::SetSetting(const SourceSetting &setting)
{
QVariant variant;
variant.setValue(setting);
_settings->setCurrentIndex(_settings->findData(variant));
}
void SourceSettingSelection::SelectionIdxChanged(int idx)
{
if (idx == -1) {
return;
}
auto setting = _settings->itemData(idx).value<SourceSetting>();
if (setting._longDescription.empty()) {
_tooltip->setToolTip("");
_tooltip->hide();
} else {
_tooltip->setToolTip(
QString::fromStdString(setting._longDescription));
_tooltip->show();
}
emit SelectionChanged(setting);
}
void SourceSettingSelection::Populate(const OBSWeakSource &source)
{
OBSSourceAutoRelease s = obs_weak_source_get_source(source);
auto settings = GetSoruceSettings(s);
for (const auto &setting : settings) {
QVariant variant;
variant.setValue(setting);
_settings->addItem(QString::fromStdString(setting._description),
variant);
}
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@@ -0,0 +1,57 @@
#pragma once
#include "filter-combo-box.hpp"
#include <obs.hpp>
#include <string>
#include <vector>
#include <QWidget>
#include <QLabel>
namespace advss {
class SourceSetting {
public:
SourceSetting() = default;
SourceSetting(const std::string &id, const std::string &description,
const std::string &longDescription = "");
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetID() const { return _id; }
bool operator==(const SourceSetting &other) const;
private:
std::string _id = "";
std::string _description = "";
std::string _longDescription = "";
friend class SourceSettingSelection;
};
std::vector<SourceSetting> GetSoruceSettings(obs_source_t *source);
std::string GetSourceSettingValue(const OBSWeakSource &source,
const SourceSetting &setting);
void SetSourceSetting(obs_source_t *source, const SourceSetting &setting,
const std::string &value);
class SourceSettingSelection : public QWidget {
Q_OBJECT
public:
SourceSettingSelection(QWidget *parent = nullptr);
void SetSource(const OBSWeakSource &);
void SetSetting(const SourceSetting &);
private slots:
void SelectionIdxChanged(int);
signals:
void SelectionChanged(const SourceSetting &);
private:
void Populate(const OBSWeakSource &);
FilterComboBox *_settings;
QLabel *_tooltip;
};
} // namespace advss

467
src/utils/temp-variable.cpp Normal file
View File

@@ -0,0 +1,467 @@
#include "temp-variable.hpp"
#include "advanced-scene-switcher.hpp"
#include "macro.hpp"
#include "macro-segment.hpp"
#include "switcher-data.hpp"
#include "utility.hpp"
#include <QVariant>
#include <QAbstractItemView>
Q_DECLARE_METATYPE(advss::TempVariableRef);
namespace advss {
TempVariable::TempVariable(const std::string &id, const std::string &name,
const std::string &description,
const std::shared_ptr<MacroSegment> &segment)
: _id(id), _name(name), _description(description), _segment(segment)
{
}
TempVariable::TempVariable(const TempVariable &other) noexcept
{
_id = other._id;
_value = other._value;
_name = other._name;
_description = other._description;
_valueIsValid = other._valueIsValid;
_segment = other._segment;
std::lock_guard<std::mutex> lock(other._lastValuesMutex);
_lastValues = other._lastValues;
}
TempVariable::TempVariable(const TempVariable &&other) noexcept
{
_id = other._id;
_value = other._value;
_name = other._name;
_description = other._description;
_valueIsValid = other._valueIsValid;
_segment = other._segment;
std::lock_guard<std::mutex> lock(other._lastValuesMutex);
_lastValues = other._lastValues;
}
TempVariable &TempVariable::operator=(const TempVariable &other) noexcept
{
if (this != &other) {
_id = other._id;
_value = other._value;
_name = other._name;
_description = other._description;
_valueIsValid = other._valueIsValid;
_segment = other._segment;
std::lock_guard<std::mutex> lockOther(other._lastValuesMutex);
std::lock_guard<std::mutex> lock(_lastValuesMutex);
_lastValues = other._lastValues;
}
return *this;
}
TempVariable &TempVariable::operator=(const TempVariable &&other) noexcept
{
if (this != &other) {
_id = other._id;
_value = other._value;
_name = other._name;
_description = other._description;
_valueIsValid = other._valueIsValid;
_segment = other._segment;
std::lock_guard<std::mutex> lockOther(other._lastValuesMutex);
std::lock_guard<std::mutex> lock(_lastValuesMutex);
_lastValues = other._lastValues;
}
return *this;
}
std::optional<std::string> TempVariable::Value() const
{
if (!_valueIsValid) {
return {};
}
return _value;
}
void TempVariable::SetValue(const std::string &val)
{
_valueIsValid = true;
if (_value == val) {
return;
}
_value = val;
std::lock_guard<std::mutex> lock(_lastValuesMutex);
if (_lastValues.size() >= 3) {
_lastValues.erase(_lastValues.begin());
}
_lastValues.push_back(val);
}
void TempVariable::InvalidateValue()
{
_valueIsValid = false;
}
TempVariableRef TempVariable::GetRef() const
{
TempVariableRef ref;
ref._id = _id;
ref._segment = _segment;
return ref;
}
TempVariableRef::SegmentType TempVariableRef::GetType() const
{
auto segment = _segment.lock();
if (!segment) {
return SegmentType::NONE;
}
auto macro = segment->GetMacro();
if (!macro) {
return SegmentType::NONE;
}
if (std::find(macro->Conditions().begin(), macro->Conditions().end(),
segment) != macro->Conditions().end()) {
return SegmentType::CONDITION;
}
if (std::find(macro->Actions().begin(), macro->Actions().end(),
segment) != macro->Actions().end()) {
return SegmentType::ACTION;
}
if (std::find(macro->ElseActions().begin(), macro->ElseActions().end(),
segment) != macro->ElseActions().end()) {
return SegmentType::ELSEACTION;
}
return SegmentType::NONE;
}
int TempVariableRef::GetIdx() const
{
auto segment = _segment.lock();
if (!segment) {
return -1;
}
return segment->GetIndex();
}
void TempVariableRef::Save(obs_data_t *obj, const char *name) const
{
OBSDataAutoRelease data = obs_data_create();
auto type = GetType();
obs_data_set_int(data, "type", static_cast<int>(type));
obs_data_set_int(data, "idx", GetIdx());
obs_data_set_string(data, "id", _id.c_str());
obs_data_set_obj(obj, name, data);
}
void TempVariableRef::Load(obs_data_t *obj, Macro *macro, const char *name)
{
if (!macro) {
_segment.reset();
return;
}
OBSDataAutoRelease data = obs_data_get_obj(obj, name);
int idx = obs_data_get_int(data, "idx");
_id = obs_data_get_string(data, "id");
const auto type =
static_cast<SegmentType>(obs_data_get_int(data, "type"));
switcher->AddPostLoadStep([this, idx, type, macro]() {
this->PostLoad(idx, type, macro);
});
}
void TempVariableRef::PostLoad(int idx, SegmentType type, Macro *macro)
{
std::deque<std::shared_ptr<MacroSegment>> segments;
switch (type) {
case SegmentType::NONE:
_segment.reset();
return;
case SegmentType::CONDITION:
segments = {macro->Conditions().begin(),
macro->Conditions().end()};
break;
case SegmentType::ACTION:
segments = {macro->Actions().begin(), macro->Actions().end()};
break;
case SegmentType::ELSEACTION:
segments = {macro->ElseActions().begin(),
macro->ElseActions().end()};
break;
default:
break;
}
if (idx < 0 || idx >= (int)segments.size()) {
_segment.reset();
return;
}
_segment = segments[idx];
}
std::optional<const TempVariable>
TempVariableRef::GetTempVariable(Macro *macro) const
{
if (!macro) {
return {};
}
auto segment = _segment.lock();
if (!segment) {
return {};
}
return macro->GetTempVar(segment.get(), _id);
}
bool TempVariableRef::operator==(const TempVariableRef &other) const
{
if (_id != other._id) {
return false;
}
auto segment = _segment.lock();
if (!segment) {
return false;
}
return segment == other._segment.lock();
}
TempVariableSelection::TempVariableSelection(QWidget *parent)
: QWidget(parent),
_selection(new FilterComboBox(
this, obs_module_text("AdvSceneSwitcher.tempVar.select"))),
_info(new AutoUpdateTooltipLabel(this, [this]() {
return SetupInfoLabel();
}))
{
QString path = GetThemeTypeName() == "Light"
? ":/res/images/help.svg"
: ":/res/images/help_light.svg";
QIcon icon(path);
QPixmap pixmap = icon.pixmap(QSize(16, 16));
_info->setPixmap(pixmap);
_info->hide();
_selection->setDuplicatesEnabled(true);
PopulateSelection();
QWidget::connect(_selection, SIGNAL(currentIndexChanged(int)), this,
SLOT(SelectionIdxChanged(int)));
QWidget::connect(_selection, SIGNAL(highlighted(int)), this,
SLOT(HighlightChanged(int)));
QWidget::connect(window(), SIGNAL(MacroSegmentOrderChanged()), this,
SLOT(MacroSegmentsChanged()));
QWidget::connect(window(), SIGNAL(SegmentTempVarsChanged()), this,
SLOT(SegmentTempVarsChanged()));
auto layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_selection);
layout->addWidget(_info);
setLayout(layout);
}
void TempVariableSelection::SelectionIdxChanged(int idx)
{
if (idx == -1) {
return;
}
auto var = _selection->itemData(idx).value<TempVariableRef>();
emit SelectionChanged(var);
HighlightSelection(var);
SetupInfoLabel();
}
void TempVariableSelection::SetVariable(const TempVariableRef &var)
{
const QSignalBlocker b(_selection);
QVariant variant;
variant.setValue(var);
_selection->setCurrentIndex(_selection->findData(variant));
SetupInfoLabel();
}
void TempVariableSelection::MacroSegmentsChanged()
{
auto currentSelection = _selection->itemData(_selection->currentIndex())
.value<TempVariableRef>();
const QSignalBlocker b(_selection);
_selection->clear();
PopulateSelection();
SetVariable(currentSelection);
}
void TempVariableSelection::SegmentTempVarsChanged()
{
MacroSegmentsChanged();
}
void TempVariableSelection::HighlightChanged(int idx)
{
auto var = _selection->itemData(idx).value<TempVariableRef>();
HighlightSelection(var);
}
void TempVariableSelection::PopulateSelection()
{
auto advssWindow = qobject_cast<AdvSceneSwitcher *>(window());
if (!advssWindow) {
return;
}
auto macro = advssWindow->GetSelectedMacro();
if (!macro) {
return;
}
auto vars = macro->GetTempVars(GetSegment());
for (const auto &var : vars) {
QVariant variant;
variant.setValue(var.GetRef());
_selection->addItem(QString::fromStdString(var.Name()),
variant);
}
// Make sure content is readable when expanding combobox
auto view = _selection->view();
if (view) {
view->setMinimumWidth(view->sizeHintForColumn(0));
_selection->setMinimumWidth(view->sizeHintForColumn(0));
view->updateGeometry();
_selection->updateGeometry();
}
adjustSize();
updateGeometry();
}
void TempVariableSelection::HighlightSelection(const TempVariableRef &var)
{
auto advssWindow = qobject_cast<AdvSceneSwitcher *>(window());
if (!advssWindow) {
return;
}
auto type = var.GetType();
switch (type) {
case TempVariableRef::SegmentType::NONE:
return;
case TempVariableRef::SegmentType::CONDITION:
advssWindow->HighlightCondition(var.GetIdx(), Qt::white);
return;
case TempVariableRef::SegmentType::ACTION:
advssWindow->HighlightAction(var.GetIdx(), Qt::white);
return;
case TempVariableRef::SegmentType::ELSEACTION:
advssWindow->HighlightElseAction(var.GetIdx(), Qt::white);
return;
default:
break;
}
}
QString TempVariableSelection::SetupInfoLabel()
{
auto currentSelection = _selection->itemData(_selection->currentIndex())
.value<TempVariableRef>();
auto advssWindow = qobject_cast<AdvSceneSwitcher *>(window());
if (!advssWindow) {
_info->setToolTip("");
_info->hide();
return "";
}
auto macro = advssWindow->GetSelectedMacro();
if (!macro) {
_info->setToolTip("");
_info->hide();
return "";
}
auto var = currentSelection.GetTempVariable(macro.get());
if (!var) {
_info->setToolTip("");
_info->hide();
return "";
}
auto description = QString::fromStdString(var->_description);
std::lock_guard<std::mutex> lock(var->_lastValuesMutex);
if (!var->_lastValues.empty()) {
if (!description.isEmpty()) {
description += QString("\n\n");
}
description +=
QString(obs_module_text(
"AdvSceneSwitcher.tempVar.selectionInfo.lastValues")) +
"\n";
for (const auto &value : var->_lastValues) {
description += "\n" + QString::fromStdString(value);
}
}
_info->setToolTip(description);
_info->setVisible(!description.isEmpty());
return description;
}
MacroSegment *TempVariableSelection::GetSegment() const
{
const QWidget *widget = this;
{
while (widget) {
if (qobject_cast<const MacroSegmentEdit *>(widget)) {
break;
}
widget = widget->parentWidget();
}
}
if (!widget) {
return nullptr;
}
auto segmentWidget = qobject_cast<const MacroSegmentEdit *>(widget);
return segmentWidget->Data().get();
}
void NotifyUIAboutTempVarChange()
{
obs_queue_task(
OBS_TASK_UI,
[](void *) {
if (!GetSwitcher()->settingsWindowOpened) {
return;
}
AdvSceneSwitcher::window->SegmentTempVarsChanged();
},
nullptr, false);
}
AutoUpdateTooltipLabel::AutoUpdateTooltipLabel(
QWidget *parent, std::function<QString()> updateFunc)
: QLabel(parent), _updateFunc(updateFunc), _timer(new QTimer(this))
{
connect(_timer, &QTimer::timeout, this,
&AutoUpdateTooltipLabel::UpdateTooltip);
}
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
void AutoUpdateTooltipLabel::enterEvent(QEnterEvent *event)
#else
void AutoUpdateTooltipLabel::enterEvent(QEvent *event)
#endif
{
_timer->start(1000);
QLabel::enterEvent(event);
}
void AutoUpdateTooltipLabel::leaveEvent(QEvent *event)
{
_timer->stop();
QLabel::leaveEvent(event);
}
void AutoUpdateTooltipLabel::UpdateTooltip()
{
setToolTip(_updateFunc());
}
} // namespace advss

132
src/utils/temp-variable.hpp Normal file
View File

@@ -0,0 +1,132 @@
#pragma once
#include "filter-combo-box.hpp"
#include <obs.hpp>
#include <mutex>
#include <optional>
#include <QEnterEvent>
#include <QEvent>
#include <QLabel>
#include <QTimer>
#include <QStringList>
#include <string>
namespace advss {
class Macro;
class MacroSegment;
class TempVariableRef;
class TempVariableSelection;
// TempVariables are variables that are local to a given macro.
// They can be created and used by macro segments.
//
// For example, a condition could create the TempVariable holding the property
// "user name" which then might be used by a action to change the settings of a
// source in OBS.
class TempVariable {
public:
TempVariable(const std::string &id, const std::string &name,
const std::string &description,
const std::shared_ptr<MacroSegment> &);
TempVariable() = default;
~TempVariable() = default;
TempVariable(const TempVariable &) noexcept;
TempVariable(const TempVariable &&) noexcept;
TempVariable &operator=(const TempVariable &) noexcept;
TempVariable &operator=(const TempVariable &&) noexcept;
std::string ID() const { return _id; }
std::weak_ptr<MacroSegment> Segment() const { return _segment; }
std::string Name() const { return _name; }
std::optional<std::string> Value() const;
void SetValue(const std::string &val);
void InvalidateValue();
TempVariableRef GetRef() const;
private:
std::string _id = "";
std::string _value = "";
std::string _name = "";
std::string _description = "";
mutable std::mutex _lastValuesMutex;
std::vector<std::string> _lastValues;
bool _valueIsValid = false;
std::weak_ptr<MacroSegment> _segment;
friend TempVariableSelection;
friend TempVariableRef;
};
class TempVariableRef {
public:
void Save(obs_data_t *, const char *name = "tempVar") const;
void Load(obs_data_t *, Macro *, const char *name = "tempVar");
std::optional<const TempVariable> GetTempVariable(Macro *) const;
bool operator==(const TempVariableRef &other) const;
private:
enum class SegmentType { NONE, CONDITION, ACTION, ELSEACTION };
SegmentType GetType() const;
int GetIdx() const;
void PostLoad(int idx, SegmentType, Macro *);
std::string _id = "";
std::weak_ptr<MacroSegment> _segment;
friend TempVariable;
friend TempVariableSelection;
};
class AutoUpdateTooltipLabel : public QLabel {
Q_OBJECT
public:
AutoUpdateTooltipLabel(QWidget *parent,
std::function<QString()> updateFunc);
protected:
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
void enterEvent(QEnterEvent *event) override;
#else
void enterEvent(QEvent *event) override;
#endif
void leaveEvent(QEvent *event) override;
private slots:
void UpdateTooltip();
private:
std::function<QString()> _updateFunc;
QTimer *_timer;
};
class TempVariableSelection : public QWidget {
Q_OBJECT
public:
TempVariableSelection(QWidget *parent);
void SetVariable(const TempVariableRef &);
private slots:
void SelectionIdxChanged(int);
void MacroSegmentsChanged();
void SegmentTempVarsChanged();
void HighlightChanged(int);
signals:
void SelectionChanged(const TempVariableRef &);
private:
void PopulateSelection();
void HighlightSelection(const TempVariableRef &);
QString SetupInfoLabel();
MacroSegment *GetSegment() const;
FilterComboBox *_selection;
AutoUpdateTooltipLabel *_info;
};
void NotifyUIAboutTempVarChange();
} // namespace advss

View File

@@ -31,6 +31,7 @@
#include <unordered_map>
#include <regex>
#include <set>
#include <nlohmann/json.hpp>
namespace advss {
@@ -701,6 +702,24 @@ void LoadSplitterPos(QList<int> &sizes, obs_data_t *obj, const std::string name)
obs_data_array_release(array);
}
std::optional<std::string> GetJsonField(const std::string &jsonStr,
const std::string &fieldToExtract)
{
try {
nlohmann::json json = nlohmann::json::parse(jsonStr);
auto it = json.find(fieldToExtract);
if (it == json.end()) {
return {};
}
if (it->is_string()) {
return it->get<std::string>();
}
return it->dump();
} catch (const nlohmann::json::exception &) {
return {};
}
return {};
}
QStringList GetSourceNames()
{
auto sourceEnum = [](void *param, obs_source_t *source) -> bool /* -- */

View File

@@ -11,6 +11,7 @@
#include <obs-frontend-api.h>
#include <deque>
#include <unordered_map>
#include <optional>
namespace advss {
@@ -134,6 +135,8 @@ void SaveSplitterPos(const QList<int> &sizes, obs_data_t *obj,
const std::string name);
void LoadSplitterPos(QList<int> &sizes, obs_data_t *obj,
const std::string name);
std::optional<std::string> GetJsonField(const std::string &json,
const std::string &id);
/* Legacy helpers */

View File

@@ -1,8 +1,18 @@
#include "variable-line-edit.hpp"
#include <obs-module.h>
#include <utility.hpp>
namespace advss {
VariableLineEdit::VariableLineEdit(QWidget *parent) : QLineEdit(parent) {}
VariableLineEdit::VariableLineEdit(QWidget *parent) : QLineEdit(parent)
{
QLineEdit::setToolTip(
obs_module_text("AdvSceneSwitcher.tooltip.availableVariables"));
QWidget::connect(this, SIGNAL(inputRejected()), this,
SLOT(DisplayValidationMessages()));
}
void VariableLineEdit::setText(const QString &string)
{
@@ -14,4 +24,23 @@ void VariableLineEdit::setText(const StringVariable &string)
QLineEdit::setText(QString::fromStdString(string.UnresolvedValue()));
}
void VariableLineEdit::setToolTip(const QString &string)
{
QLineEdit::setToolTip(
string + "\n" +
obs_module_text("AdvSceneSwitcher.tooltip.availableVariables"));
}
void VariableLineEdit::DisplayValidationMessages()
{
int maxLength = QLineEdit::maxLength();
if (QLineEdit::text().length() == maxLength) {
DisplayMessage(
QString(obs_module_text(
"AdvSceneSwitcher.validation.text.maxLength"))
.arg(QString::number(maxLength)));
}
}
} // namespace advss

View File

@@ -11,6 +11,10 @@ public:
VariableLineEdit(QWidget *parent);
void setText(const QString &);
void setText(const StringVariable &);
void setToolTip(const QString &string);
private slots:
void DisplayValidationMessages();
private:
};

View File

@@ -63,6 +63,12 @@ const char *StringVariable::c_str() const
return _resolvedValue.c_str();
}
bool StringVariable::empty() const
{
Resolve();
return _resolvedValue.empty();
}
std::string SubstitueVariables(std::string str)
{
if (!switcher) {

View File

@@ -20,6 +20,7 @@ public:
void operator=(const char *value);
const char *c_str();
const char *c_str() const;
bool empty() const;
const std::string &UnresolvedValue() const { return _value; }

View File

@@ -1,11 +1,16 @@
#include "variable-text-edit.hpp"
#include "switcher-data.hpp"
#include <obs-module.h>
namespace advss {
VariableTextEdit::VariableTextEdit(QWidget *parent)
: ResizingPlainTextEdit(parent)
VariableTextEdit::VariableTextEdit(QWidget *parent, const int scrollAt,
const int minLines, const int paddingLines)
: ResizingPlainTextEdit(parent, scrollAt, minLines, paddingLines)
{
QPlainTextEdit::setToolTip(
obs_module_text("AdvSceneSwitcher.tooltip.availableVariables"));
}
void VariableTextEdit::setPlainText(const QString &string)
@@ -19,4 +24,11 @@ void VariableTextEdit::setPlainText(const StringVariable &string)
QString::fromStdString(string.UnresolvedValue()));
}
void VariableTextEdit::setToolTip(const QString &string)
{
QPlainTextEdit::setToolTip(
string + "\n" +
obs_module_text("AdvSceneSwitcher.tooltip.availableVariables"));
}
} // namespace advss

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