From 6558792e73fd6f511a2dad0de4e6b17d9634d5e5 Mon Sep 17 00:00:00 2001 From: Przemek Pawlas <3606072+Destroy666x@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:09:29 +0200 Subject: [PATCH] Add Twitch send chat announcement and emote-only mode actions (#890) --- data/locale/en-US.ini | 19 +- data/locale/fr-FR.ini | 69 ++++--- .../twitch/macro-action-twitch.cpp | 175 ++++++++++++++++-- .../twitch/macro-action-twitch.hpp | 32 +++- src/macro-external/twitch/token.cpp | 7 +- 5 files changed, 241 insertions(+), 61 deletions(-) diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 0ccdc787..42c2fdae 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -783,12 +783,21 @@ 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.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.entry.line1="On{{account}}{{actions}}{{streamTitle}}{{category}}{{manualCategorySearch}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}" 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.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" @@ -943,7 +952,9 @@ AdvSceneSwitcher.twitchToken.analytics.readGames="View analytics data for the ga 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.clips.edit="Create clips from channel's broadcasts." +AdvSceneSwitcher.twitchToken.moderator.manageAnnouncements="Manage channel's chat announcements." +AdvSceneSwitcher.twitchToken.moderator.manageChatSettings="Manage channel's chat settings, such as emote-only or slow mode." AdvSceneSwitcher.twitchCategories.fetchStart="Fetching stream categories ..." AdvSceneSwitcher.twitchCategories.fetchStatus="Got %1 stream categories." diff --git a/data/locale/fr-FR.ini b/data/locale/fr-FR.ini index 061a614a..2d3ae40d 100644 --- a/data/locale/fr-FR.ini +++ b/data/locale/fr-FR.ini @@ -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é" @@ -426,7 +426,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 +434,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" @@ -594,15 +594,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 +615,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 +630,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 +654,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 +674,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 +710,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,17 +723,17 @@ 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" @@ -741,7 +741,7 @@ 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.entry.line1="Sur{{account}}{{actions}}{{streamTitle}}{{category}}{{manualCategorySearch}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementMessage}}{{announcementColor}}" 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" @@ -752,10 +752,10 @@ 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="

Ces paramètres n'affectent que les transitions provoquées par le commutateur de scènes - Consultez la Table de transitions si vous souhaitez les configurer pour des changements de scène manuels.
Les paramètres définis ici ont la priorité sur les paramètres de transition configurés ailleurs dans le commutateur de scènes.

Cliquez sur le symbole plus ci-dessous pour ajouter une nouvelle entrée.

" 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 +766,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 +784,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 +937,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" @@ -1127,7 +1127,6 @@ AdvSceneSwitcher.twitchToken.analytics.readGames="Afficher les données d'analys 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.twitchCategories.fetchStart="Récupération des catégories de streams..." AdvSceneSwitcher.twitchCategories.fetchStatus="Obtenu %1 catégories de streams." diff --git a/src/macro-external/twitch/macro-action-twitch.cpp b/src/macro-external/twitch/macro-action-twitch.cpp index 689919f5..172f20cf 100644 --- a/src/macro-external/twitch/macro-action-twitch.cpp +++ b/src/macro-external/twitch/macro-action-twitch.cpp @@ -24,6 +24,35 @@ const static std::map 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"}, +}; + +const static std::map + 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 + 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 +122,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() + @@ -129,6 +159,47 @@ void MacroActionTwitch::StartCommercial( } } +void MacroActionTwitch::SendChatAnnouncement( + const std::shared_ptr &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 &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); + } +} + bool MacroActionTwitch::PerformAction() { auto token = _token.lock(); @@ -152,6 +223,15 @@ 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; default: break; } @@ -183,6 +263,9 @@ 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(_announcementColor)); return true; } @@ -196,6 +279,9 @@ bool MacroActionTwitch::Load(obs_data_t *obj) _markerDescription.Load(obj, "markerDescription"); _clipHasDelay = obs_data_get_bool(obj, "clipHasDelay"); _duration.Load(obj); + _announcementMessage.Load(obj, "announcementMessage"); + _announcementColor = static_cast( + obs_data_get_int(obj, "announcementColor")); return true; } @@ -212,6 +298,10 @@ 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"}}, }; auto token = _token.lock(); if (!token) { @@ -229,11 +319,21 @@ static inline void populateActionSelection(QComboBox *list) } } +static inline void populateAnnouncementColorSelection(QComboBox *list) +{ + for (const auto &[_, name] : announcementColors) { + list->addItem(obs_module_text(name.c_str())); + } +} + MacroActionTwitchEdit::MacroActionTwitchEdit( QWidget *parent, std::shared_ptr entryData) : QWidget(parent), + _layout(new QHBoxLayout()), _actions(new QComboBox()), _tokens(new TwitchConnectionSelection()), + _tokenPermissionWarning(new QLabel(obs_module_text( + "AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient"))), _streamTitle(new VariableLineEdit(this)), _category(new TwitchCategorySelection(this)), _manualCategorySearch(new TwitchCategorySearchButton()), @@ -241,9 +341,8 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( _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)) { _streamTitle->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); @@ -251,16 +350,22 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( _markerDescription->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); _markerDescription->setMaxLength(140); + _announcementMessage->setSizePolicy(QSizePolicy::MinimumExpanding, + QSizePolicy::Preferred); 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,26 +374,31 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( QWidget::connect(_markerDescription, SIGNAL(editingFinished()), this, SLOT(MarkerDescriptionChanged())); QObject::connect(_clipHasDelay, SIGNAL(stateChanged(int)), this, - SLOT(HasClipDelayChanged(const Duration &))); + SLOT(HasClipDelayChanged(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))); - 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}}); + PlaceWidgets( + obs_module_text("AdvSceneSwitcher.action.twitch.entry.line1"), + _layout, + {{"{{account}}", _tokens}, + {"{{actions}}", _actions}, + {"{{streamTitle}}", _streamTitle}, + {"{{category}}", _category}, + {"{{manualCategorySearch}}", _manualCategorySearch}, + {"{{markerDescription}}", _markerDescription}, + {"{{clipHasDelay}}", _clipHasDelay}, + {"{{duration}}", _duration}, + {"{{announcementColor}}", _announcementColor}}); _layout->setContentsMargins(0, 0, 0, 0); auto mainLayout = new QVBoxLayout(); mainLayout->addLayout(_layout); + mainLayout->addWidget(_announcementMessage); mainLayout->addWidget(_tokenPermissionWarning); setLayout(mainLayout); @@ -364,6 +474,31 @@ void MacroActionTwitchEdit::DurationChanged(const Duration &duration) _entryData->_duration = duration; } +void MacroActionTwitchEdit::AnnouncementMessageChanged() +{ + 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(index); +} + void MacroActionTwitchEdit::CheckTokenPermissions() { _tokenPermissionWarning->setVisible( @@ -386,6 +521,10 @@ void MacroActionTwitchEdit::SetupWidgetVisibility() MacroActionTwitch::Action::CLIP); _duration->setVisible(_entryData->_action == MacroActionTwitch::Action::COMMERCIAL); + _announcementMessage->setVisible( + _entryData->_action == MacroActionTwitch::Action::ANNOUNCEMENT); + _announcementColor->setVisible(_entryData->_action == + MacroActionTwitch::Action::ANNOUNCEMENT); if (_entryData->_action == MacroActionTwitch::Action::TITLE || _entryData->_action == MacroActionTwitch::Action::MARKER) { @@ -416,6 +555,10 @@ void MacroActionTwitchEdit::UpdateEntryData() _markerDescription->setText(_entryData->_markerDescription); _clipHasDelay->setChecked(_entryData->_clipHasDelay); _duration->SetDuration(_entryData->_duration); + _announcementMessage->setPlainText(_entryData->_announcementMessage); + _announcementColor->setCurrentIndex( + static_cast(_entryData->_announcementColor)); + SetupWidgetVisibility(); } diff --git a/src/macro-external/twitch/macro-action-twitch.hpp b/src/macro-external/twitch/macro-action-twitch.hpp index 3e1a6ada..319c0f09 100644 --- a/src/macro-external/twitch/macro-action-twitch.hpp +++ b/src/macro-external/twitch/macro-action-twitch.hpp @@ -4,6 +4,7 @@ #include "category-selection.hpp" #include +#include #include namespace advss { @@ -29,6 +30,17 @@ public: MARKER, CLIP, COMMERCIAL, + ANNOUNCEMENT, + ENABLE_EMOTE_ONLY, + DISABLE_EMOTE_ONLY, + }; + + enum class AnnouncementColor { + PRIMARY, + BLUE, + GREEN, + ORANGE, + PURPLE, }; Action _action = Action::TITLE; @@ -40,6 +52,9 @@ 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; private: void SetStreamTitle(const std::shared_ptr &) const; @@ -47,6 +62,9 @@ private: void CreateStreamMarker(const std::shared_ptr &) const; void CreateStreamClip(const std::shared_ptr &) const; void StartCommercial(const std::shared_ptr &) const; + void SendChatAnnouncement(const std::shared_ptr &) const; + void SetChatEmoteOnlyMode(const std::shared_ptr &, + bool enable) const; static bool _registered; static const std::string id; @@ -71,12 +89,14 @@ public: 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); signals: void HeaderInfoChanged(const QString &); @@ -87,17 +107,21 @@ protected: private: void SetupWidgetVisibility(); + QHBoxLayout *_layout; QComboBox *_actions; TwitchConnectionSelection *_tokens; + QLabel *_tokenPermissionWarning; + QTimer _tokenPermissionCheckTimer; + VariableLineEdit *_streamTitle; TwitchCategorySelection *_category; TwitchCategorySearchButton *_manualCategorySearch; VariableLineEdit *_markerDescription; QCheckBox *_clipHasDelay; DurationSelection *_duration; - QHBoxLayout *_layout; - QLabel *_tokenPermissionWarning; - QTimer _tokenPermissionCheckTimer; + VariableTextEdit *_announcementMessage; + QComboBox *_announcementColor; + bool _loading = true; }; diff --git a/src/macro-external/twitch/token.cpp b/src/macro-external/twitch/token.cpp index 7c8e40c1..234f1502 100644 --- a/src/macro-external/twitch/token.cpp +++ b/src/macro-external/twitch/token.cpp @@ -22,10 +22,13 @@ const std::unordered_map TokenOption::apiIdToLocale{ {"channel:manage:broadcast", "AdvSceneSwitcher.twitchToken.channel.manageBroadcast"}, - {"clips:edit", "AdvSceneSwitcher.twitchToken.channel.createClip"}, + {"clips:edit", "AdvSceneSwitcher.twitchToken.clips.edit"}, {"channel:edit:commercial", "AdvSceneSwitcher.twitchToken.channel.startCommercial"}, -}; + {"moderator:manage:announcements", + "AdvSceneSwitcher.twitchToken.moderator.manageAnnouncements"}, + {"moderator:manage:chat_settings", + "AdvSceneSwitcher.twitchToken.moderator.manageChatSettings"}}; static void saveConnections(obs_data_t *obj); static void loadConnections(obs_data_t *obj);