mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-29 04:36:21 -05:00
Compare commits
61 Commits
1.24.0-bet
...
recording
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21a00d47cc | ||
|
|
3805aaeeec | ||
|
|
50dc404689 | ||
|
|
975eacbc7e | ||
|
|
7e56dc35b8 | ||
|
|
b3566d98bf | ||
|
|
15379a8e20 | ||
|
|
1e178f4223 | ||
|
|
c16a7b93ea | ||
|
|
f89bb1a750 | ||
|
|
79961aabe4 | ||
|
|
4686fc2b01 | ||
|
|
569b915c07 | ||
|
|
324d3fe7a5 | ||
|
|
ae8d0bf5cb | ||
|
|
17efa82057 | ||
|
|
c17523a476 | ||
|
|
33635d6991 | ||
|
|
d290dbe86b | ||
|
|
c1f5c49eda | ||
|
|
c3f4caa6f3 | ||
|
|
d4c7b0e455 | ||
|
|
b0cb52fe90 | ||
|
|
386f831cd4 | ||
|
|
449daa80cc | ||
|
|
3003547b83 | ||
|
|
1664eb672f | ||
|
|
0939dcacb6 | ||
|
|
54b81d1a42 | ||
|
|
626606100f | ||
|
|
53a0b76e8c | ||
|
|
e16ac9400a | ||
|
|
94060c236b | ||
|
|
890792c270 | ||
|
|
7bc1fc3d0d | ||
|
|
9ea90ed61b | ||
|
|
ce40b80d90 | ||
|
|
53fcd94c5a | ||
|
|
ecb6247959 | ||
|
|
3b4cb6934d | ||
|
|
babe93a6ce | ||
|
|
1e9173699c | ||
|
|
37ba7aa2ca | ||
|
|
a83d2d9910 | ||
|
|
0449064f5d | ||
|
|
6cf1073a3c | ||
|
|
9ac9171206 | ||
|
|
25ce8cd2e4 | ||
|
|
e50522ab89 | ||
|
|
4801502f32 | ||
|
|
e13e4b5ddd | ||
|
|
de8da553f1 | ||
|
|
6558792e73 | ||
|
|
ea5e92017e | ||
|
|
46199efb58 | ||
|
|
8a6eae6c97 | ||
|
|
67eef8a004 | ||
|
|
b5341e5cde | ||
|
|
ac6140b17a | ||
|
|
b1489d98f0 | ||
|
|
3b7119ec58 |
2
.github/scripts/Build-Windows.ps1
vendored
2
.github/scripts/Build-Windows.ps1
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/scripts/utils.zsh/check_packages
vendored
2
.github/scripts/utils.zsh/check_packages
vendored
@@ -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
3
.gitmodules
vendored
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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}}"
|
||||
|
||||
@@ -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 channel’s 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 channel’s 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 broadcaster’s 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 user’s 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 won’t 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 can’t 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 can’t 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 isn’t 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 isn’t live or max_per_stream isn’t 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 isn’t live or max_per_stream isn’t 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 charity’s logo. The image’s 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 campaign’s 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 Mode’s 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 Mode’s 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 Mode’s 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 broadcaster’s stream delay setting, in seconds.\nThis field’s 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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
2
deps/asio
vendored
Submodule deps/asio updated: b73dc1d2c0...c465349fa5
1
deps/json
vendored
Submodule
1
deps/json
vendored
Submodule
Submodule deps/json added at bc889afb4c
2
deps/websocketpp
vendored
2
deps/websocketpp
vendored
Submodule deps/websocketpp updated: c6d7e295bf...56123c8759
@@ -325,6 +325,9 @@ void SwitcherData::SetPreconditions()
|
||||
cursorPosChanged = cursorPos.first != switcher->lastCursorPos.first ||
|
||||
cursorPos.second != switcher->lastCursorPos.second;
|
||||
lastCursorPos = GetCursorPos();
|
||||
|
||||
// Macro
|
||||
InvalidateMacroTempVarValues();
|
||||
}
|
||||
|
||||
void ClearWebsocketMessages();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 ==
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -75,7 +75,7 @@ private slots:
|
||||
|
||||
private:
|
||||
void SetLogicSelection();
|
||||
std::shared_ptr<MacroSegment> Data();
|
||||
std::shared_ptr<MacroSegment> Data() const;
|
||||
|
||||
QComboBox *_logicSelection;
|
||||
FilterComboBox *_conditionSelection;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 ®ex, 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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
368
src/macro-core/macro-condition-tempvar.cpp
Normal file
368
src/macro-core/macro-condition-tempvar.cpp
Normal 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
|
||||
100
src/macro-core/macro-condition-tempvar.hpp
Normal file
100
src/macro-core/macro-condition-tempvar.hpp
Normal 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
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -212,7 +212,7 @@ static inline int
|
||||
ModelIndexToMacroIndex(int modelIdx,
|
||||
const std::deque<std::shared_ptr<Macro>> ¯os)
|
||||
{
|
||||
assert(macros.size() >= modelIdx);
|
||||
assert((int)macros.size() >= modelIdx);
|
||||
int realIdx = 0;
|
||||
const auto &m = macros[0];
|
||||
bool inCollapsedGroup = m->IsGroup() && m->IsCollapsed();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
263
src/macro-external/twitch/channel-selection.cpp
Normal file
263
src/macro-external/twitch/channel-selection.cpp
Normal 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
|
||||
86
src/macro-external/twitch/channel-selection.hpp
Normal file
86
src/macro-external/twitch/channel-selection.hpp
Normal 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
|
||||
716
src/macro-external/twitch/chat-connection.cpp
Normal file
716
src/macro-external/twitch/chat-connection.cpp
Normal 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
|
||||
114
src/macro-external/twitch/chat-connection.hpp
Normal file
114
src/macro-external/twitch/chat-connection.hpp
Normal 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
|
||||
439
src/macro-external/twitch/event-sub.cpp
Normal file
439
src/macro-external/twitch/event-sub.cpp
Normal 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 ×tamp)
|
||||
{
|
||||
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
|
||||
100
src/macro-external/twitch/event-sub.hpp
Normal file
100
src/macro-external/twitch/event-sub.hpp
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
1454
src/macro-external/twitch/macro-condition-twitch.cpp
Normal file
1454
src/macro-external/twitch/macro-condition-twitch.cpp
Normal file
File diff suppressed because it is too large
Load Diff
191
src/macro-external/twitch/macro-condition-twitch.hpp
Normal file
191
src/macro-external/twitch/macro-condition-twitch.hpp
Normal 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
|
||||
219
src/macro-external/twitch/points-reward-selection.cpp
Normal file
219
src/macro-external/twitch/points-reward-selection.cpp
Normal 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
|
||||
74
src/macro-external/twitch/points-reward-selection.hpp
Normal file
74
src/macro-external/twitch/points-reward-selection.hpp
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ¶ms)
|
||||
{
|
||||
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 ¶ms, 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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ void PreviewDialog::Stop()
|
||||
_thread.wait();
|
||||
}
|
||||
|
||||
void PreviewDialog::closeEvent(QCloseEvent *event)
|
||||
void PreviewDialog::closeEvent(QCloseEvent *)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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()) ||
|
||||
|
||||
@@ -59,6 +59,7 @@ protected:
|
||||
std::string_view _addStr;
|
||||
std::string_view _conflictStr;
|
||||
QString _originalName;
|
||||
bool _showNameEmptyWarning = true;
|
||||
};
|
||||
|
||||
typedef bool (*SettingsCallback)(QWidget *, Item &);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
229
src/utils/source-setting.cpp
Normal file
229
src/utils/source-setting.cpp
Normal 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
|
||||
57
src/utils/source-setting.hpp
Normal file
57
src/utils/source-setting.hpp
Normal 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
467
src/utils/temp-variable.cpp
Normal 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
132
src/utils/temp-variable.hpp
Normal 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
|
||||
@@ -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 /* -- */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user