Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
9384c57f43 |
164
README.md
@@ -1,37 +1,37 @@
|
||||
# 
|
||||
|
||||
ChatRD is a chat tool and/or overlay widget that unifies messages and events from **Twitch**, **YouTube**, **TikTok**, **Kick**, **Streamlabs**, **StreamElements**, **Patreon**, **TipeeeStream**, **Ko-Fi**, **Fourthwall** (and more to come).
|
||||
ChatRD is a chat tool and/or overlay widget that unifies messages and events from **Twitch**, **YouTube**, **TikTok**, **Kick**, **Streamlabs**, **StreamElements**, **Patreon**, **TipeeeStream**, **Ko-Fi** and **Fourthwall**.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
## 🛠️ Setting it up
|
||||
|
||||
## 🚀 Features
|
||||
Make sure your **Twitch**, **YouTube** and **Kick** accounts are connected on **Streamer.bot**. Also have **TikFinity Desktop App** installed and your account on **TikTok** setup. **BOTH APPS NEED TO RUN ON THE SAME PC**.
|
||||
|
||||
- 💬 **Multi-platform chat**
|
||||
- 💬 **Multi-language events**
|
||||
- 📊 **Events and Live stats**
|
||||
- 🎨 **Customizable**
|
||||
- 💾 **Saves your settings using localStorage and Streamer.Bot's Global Varaibles**
|
||||
If you have both of these ready, follow these steps:
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Usage
|
||||
|
||||
Make sure your **Twitch** and **YouTube** accounts are connected on **Streamer.Bot** and you have **TikFinity Desktop App** installed and set up to your account on **TikTok**. **BOTH APPS NEED TO RUN ON THE SAME PC**.
|
||||
|
||||
### Streamer.Bot & TikFinity
|
||||
1. On **Streamer.Bot**, go to **Server/Clients → WebSocket Server** and make sure it is running
|
||||
2. Import the string inside the file [streamerbot-import.vortisrd](https://github.com/vortisrd/chatrd/blob/main/streamerbot-import.vortisrd) to your **Streamer.Bot** using the **Import** button at the top.
|
||||
3. Go to **Server/Clients → WebSocket Client** and make sure the *TikFinity* WebSocket is connected. If not, right-click on it and check *Auto-Connect* and *Reconnect* before clicking on *Connect*.
|
||||
4. Download [**TikFinity Desktop App**](https://tikfinity.zerody.one/) and make sure it's is opened and connected to your TikTok Account.
|
||||
1. On **Streamer.bot**, import the file [chatrd.sb](https://github.com/vortisrd/chatrd/blob/main/chatrd.sb) to your **Streamer.bot**.
|
||||
2. Go to **Server/Clients → WebSocket Server** and make sure it is running.
|
||||
5. Open the [Settings Page](https://vortisrd.github.io/chatrd) in your browser.
|
||||
6. Choose your desired options.
|
||||
7. Click **"Copy URL"**.
|
||||
8. Add the copied URL as a Browser Source in OBS. Or use it in your browser to read chat. 😊
|
||||
9. For **Streamlabs**, **StreamElements**, **Patreon**, **TipeeeStream**, **Ko-Fi** and **Fourthwall**, you need to connect them to your Streamer.Bot account to their website. Follow the tutorial links in each section presented in the [Settings Page](https://vortisrd.github.io/chatrd).
|
||||
|
||||
### TikTok Chat Setup
|
||||
---
|
||||
|
||||
## 🔊 Setting TTS with Speaker.Bot
|
||||
|
||||
1. Go to **Settings → WebSocket Server**, click on *Start Server*. Make sure to also tick the *Auto-Start* checkbox.
|
||||
2. Copy the IP and Port to ChatRD Speaker.bot fields.
|
||||
3. Go to **Settings → Speech Engine** and add the TTS Service of your preference. (Sapi5 is the Windows default).
|
||||
4. Go to **Settings → Voice Aliases**, give it a name and click **Add** right next to it.
|
||||
5. In the Left Column, click on the **SpeakerBot** you just added and on the **Speak!** section, select the voice you want to use and click **Add**. (If you're using Sapi5, I recommend using *Microsoft Zira Desktop* as a voice).
|
||||
6. Add the Alias name under the *Voice Alias* field on ChatRD.
|
||||
|
||||
---
|
||||
|
||||
## 💬 Sending Messages to TikTok
|
||||
To send messages to **TikTok** using the *Chat Field*, you need to the following on **TikFinity**:
|
||||
|
||||
1. Make sure you're connected to your TikTok Account on **TikFinity**. If you're not, go to **Setup → TikTok Login** and click on *Login to TikTok*.
|
||||
@@ -43,52 +43,23 @@ To send messages to **TikTok** using the *Chat Field*, you need to the following
|
||||
|
||||

|
||||
|
||||
|
||||
### Kick.Bot installation on Streamer.Bot
|
||||
|
||||
1. First, [download Kick.Bot from Sehelitar's repo](https://github.com/Sehelitar/Kick.bot/releases/).
|
||||
2. Unzip and copy the DLLs from **Kick.Bot** to the *dlls folder* inside **Streamer.Bot**. If it's not there, create one (name it "dlls", lowercase).
|
||||
3. Import the action from *action.txt* file (inside the ZIP folder you just unzipped) to **Streamer.Bot**.
|
||||
4. Close **Streamer.Bot** and open it again. After a few seconds, a window will appear asking you to login on *Kick*.
|
||||
5. Done! 😊
|
||||
|
||||
If **Kick.Bot** stops sending events to ChatRD, delete it's dlls in the *dlls folder*, delete the imported **Kick.Bot** action and delete the *Streamer.bot.exe.WebView2* folder. After that, reinstall **Kick.Bot** using the above instructions.
|
||||
|
||||
|
||||
#### ⚠️ KICK IS A BETA FEATURE!
|
||||
Kick doesn't offer an API like Twitch does. It's not feasible for Streamer.Bot do it in an easy manner like Twitch, YouTube, Trovo, etc.
|
||||
|
||||
[Kick.Bot](https://github.com/Sehelitar/Kick.bot/releases/) does an excelent job but there is some information missing on the payload from Kick, like *avatars*, *badges (gift sub, community, etc), viewership, etc*. And there will be some errors or some misinformation being shown. I tried my best to emulate these.
|
||||
|
||||
I couldn't test every single outcome because it's not possible to simulate the events besides Chat. I followed [Kick.Bot's](https://github.com/Sehelitar/Kick.bot/releases/) documentation and hoped for the best. 🙏
|
||||
|
||||
Also, at any point either **Kick** or **Kick.Bot** might change their stuff, so please, **be patient!** 😊
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔊 How to set TTS with Speaker.Bot
|
||||
|
||||
### Speaker.Bot Setup
|
||||
1. Go to **Settings → WebSocket Server**, click on *Start Server*. Make sure to also tick the *Auto-Start* checkbox.
|
||||
2. Go to **Settings → Speech Engine** and add the TTS Service of your preference. (Sapi5 is the Windows default).
|
||||
3. Go to **Settings → Voice Aliases**, name the voice *SpeakerBot* and click **Add** right next to it.
|
||||
4. In the Left Column, click on the **SpeakerBot** you just added and on the **Speak!** section, select the voice you want to use and click **Add**. (If you're using Sapi5, I recommend using *Microsoft Zira Desktop* as a voice).
|
||||
|
||||
### Streamer.Bot Setup
|
||||
1. Import the [streamerbot-import.vortisrd](https://github.com/vortisrd/chatrd/blob/main/streamerbot-import.vortisrd) file to your **Streamer.Bot**. There's a new action that will handle the **Speaker.Bot** integration.
|
||||
2. Go to **Integrations → Speaker.Bot**, click on *Connect*. Make sure to also tick the *Auto-Start* and *Auto-Connect* checkboxes.
|
||||
|
||||
---
|
||||
|
||||
## 💻 Commands supported by the Chat Field
|
||||
|
||||
**Commands for Twitch**
|
||||
- /me (message)
|
||||
- /clip
|
||||
- /announce (message)
|
||||
- /announceblue (message)
|
||||
- /announcegreen (message)
|
||||
- /announceorange (message)
|
||||
- /announcepurple (message)
|
||||
- /clear
|
||||
- /slow (duration in seconds)
|
||||
- /slowoff
|
||||
- /emoteonly
|
||||
- /emoteonlyoff
|
||||
- /subscribers
|
||||
- /subscribersoff
|
||||
- /commercial (duration in seconds)
|
||||
@@ -103,80 +74,51 @@ Also, at any point either **Kick** or **Kick.Bot** might change their stuff, so
|
||||
- /shoutout (user)
|
||||
- /raid (user)
|
||||
- /unraid
|
||||
- /settitle (stream title)
|
||||
- /setgame (game name)
|
||||
|
||||
**Commands for YouTube**
|
||||
- /yt/title (title)
|
||||
- /yt/description (description)
|
||||
- /yt/title (stream title)
|
||||
- /yt/timeout (userID) (duration in seconds)
|
||||
- /yt/ban (userID)
|
||||
|
||||
**Commands for Kick**
|
||||
- /kick/title (stream title)
|
||||
- /kick/category (stream category title)
|
||||
- /kick/timeout (user) (duration in seconds)
|
||||
- /kick/untimeout (user)
|
||||
- /kick/ban (user) (reason)
|
||||
- /kick/unban (user)
|
||||
|
||||
**TikTok**
|
||||
- TikTok commands are not supported yet.
|
||||
- TikTok commands are not supported.
|
||||
|
||||
**Kick**
|
||||
- Kick commands are not supported yet.
|
||||
|
||||
---
|
||||
|
||||
## 📝 To-Do List
|
||||
|
||||
- Trovo
|
||||
- LivePix
|
||||
- Tipa.Ai
|
||||
|
||||
---
|
||||
|
||||
## ❓ Frequently Asked Questions
|
||||
**- Can I use it to read my chat?**
|
||||
R: Yes you can. You can open it on your browser, use it as a chat overlay and use it as a dock in OBS.
|
||||
R: Yes you can. You can open it on your browser, use it as a chat overlay and/or use it as a dock in OBS.
|
||||
|
||||
**- What about YouTube Members Emotes?**
|
||||
R: YouTube doesn't expose their Partner Emojis to Streamer.Bot. All you can do is add the emojis manually in ChatRD. Please [read this](https://github.com/vortisrd/chatrd#about-youtube-membership-emojis).
|
||||
R: YouTube doesn't expose their Membership/Partner Emojis to Streamer.bot. You would have to add them manually on ChatRD's Members Only Emotes section.
|
||||
|
||||
What Casterlabs Caffeinated, Social Stream Ninja and Onecomme do to scrape the emotes won't work with the current way Streamer.Bot and my code works, so I had to choose between **making the user add them manually** or build a **server-sided executable (using NodeJS, Python or whatever) to read the chat as it's going or scrape the HTML code**. I don't want to add another executable on top of the user's flow, so it would be easier to use what it's currently available. **And no, I won't do any research based on what other tools do.**
|
||||
|
||||
**- Can I set TTS to read only the events I want to read?**
|
||||
R: ChatRD only reads either chats, events or both. If you want to filter the events, [I suggest you setup Speaker.Bot separately](https://github.com/vortisrd/chatrd?tab=readme-ov-file#about-speakerbot-tts-customization).
|
||||
R: No. ChatRD sends either chats, events or both to Speaker.bot.
|
||||
|
||||
**- TikTok events are not working anymore, what should I do?**
|
||||
R: Make sure your TikFinity is connected to your account and you are live. Also go to **Server/Clients → WebSocket Client** and make sure the TikFinity WebSocket is connected. [Instructions here](https://github.com/vortisrd/chatrd#streamerbot--tikfinity).
|
||||
R: Make sure your TikFinity is connected to your account and you are live.
|
||||
|
||||
**- Kick events are not working anymore, what should I do?**
|
||||
R: Try reinstalling Kick.Bot [using the following methods](https://github.com/vortisrd/chatrd?tab=readme-ov-file#kickbot-installation-on-streamerbot).
|
||||
|
||||
**- Where are my Kick sub badges? And the X or Y badges?**
|
||||
R: At the moment of this post, the WebSocket I connect to show Kick's chat doesn't expose the Badges like Twitch does. Twitch mentions the badges and sends the URL, showing it [like this](https://i.imgur.com/xwg39hO.png). **Kick doesn't, showing on code [like this](https://i.imgur.com/OtMcDzI.png)**. You can see the person is subscribed and it's a sub gifter, but it doesn't show the specified badges. I did the best I could with the other badges. When Kick improves their API, I will revisit this later.
|
||||
**- Kick events are not working or are taking too much time to show, what should I do?**
|
||||
R: Kick's API is notoriously slow on their peak usage. It's been reported on [Streamer.bot](https://discord.streamer.bot/) Discord (check html-css-js section) that sometimes it could take up to 60 seconds for the responses to be relayed. I hope in the future they throw more money into their servers.
|
||||
|
||||
**- Can you add other streaming/payment platforms?**
|
||||
R: ChatRD uses Streamer.Bot to 95% of all platform iterations. *TikFinity* is perfectly integrated via WebSockets and *Kick.Bot* adds a decent integration. So if the platform has a decent WebSocket API (not WebHooks, those need a server to be usable) and/or has any integration with Streamer.Bot, please feel free to suggest it. Other than that, there are no plans to add more platforms.
|
||||
R: ChatRD uses Streamer.Bot to 95% of all platform iterations. *TikFinity* is perfectly integrated via WebSockets. So if the platform has any integration with Streamer.bot or has a decent WebSocket API (not WebHooks), feel free to suggest it. Other than that, there are no plans to add more platforms.
|
||||
|
||||
**- Can I customize it?**
|
||||
R: If you mean visual styles, yes. [Read here](https://github.com/vortisrd/chatrd?tab=readme-ov-file#about-custom-styling).
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **⚠️ DISCLAIMERS ⚠️**
|
||||
|
||||
### About YouTube Membership Emojis
|
||||
I tried to add member emotes but **that is currently impossible due to YouTube's API not exposing Members Emotes and with that, Streamer.Bot won't be able to show them.**. So I've added a way for the users to add them manually at the overlay, with the data saved as a Streamer.Bot Global Variable.
|
||||
|
||||
What Casterlabs Caffeinated, Social Stream Ninja and Onecomme do to scrape the emotes won't work with the current way Streamer.Bot and my code works, so I had to choose between **making the user add them manually** or build a **server-sided executable (using NodeJS, Python or whatever) to read the chat as it's going or scrape the HTML code**. I don't want to add another executable on top of the user's flow, so it would be easier to use what it's currently available. **And no, I won't do any research based on what other tools do.** Tried to do that and wasted 1 week of my life doing it.
|
||||
|
||||
When YouTube decide to expose their Partner Emotes on their API, I'll come back to this.
|
||||
|
||||
### About Speaker.Bot TTS Customization
|
||||
If you want to customize what events the TTS reads, like "I want it to read sub notifications but I don't want it to read bits", it's possible. But doing that means adding an extra TTS switch for every single event for all platforms, making the setup page almost triplicate in size and bloat the code. **I won't do that**. I want to keep it simple and contained.
|
||||
|
||||
If you want to have TTS for events separately, I suggest you **Disable TTS Events** on ChatRD and setup Speaker.Bot with the things you want. 😊
|
||||
|
||||
### About Custom Styling
|
||||
The safest way to customize ChatRD is open either the Chat or the Config in your browser and use [it's Dev Tools](https://i.imgur.com/Nirwz5R.png) to look for the tags, their classes, identifiers and then style in the way you want. **You need basic CSS knowledge for that**.
|
||||
|
||||
After you finish it, paste the CSS inside the [Custom CSS field within Browser Source Property Window](https://i.imgur.com/BjvrV28.png).
|
||||
|
||||
### About Support on Changing the Javascript or other Core Files
|
||||
If you break it, you fix it. 😊
|
||||
|
||||
R: If you mean visual styles, you can add your own using the *Custom CSS* field in OBS's Browser Source Properties Window. You can use your browser Dev Tools to inspect the elements you want to change. **I won't provide support if you're planning to customize codes that could break ChatRD**.
|
||||
|
||||
---
|
||||
|
||||
@@ -184,8 +126,8 @@ If you break it, you fix it. 😊
|
||||
|
||||
Made with ❤️ by **VortisRD**
|
||||
|
||||
🔗 [GitHub](https://github.com/vortisrd) • [Twitch](https://twitch.tv/vortisrd) • [YouTube](https://youtube.com/@vortisrd) • [TikTok](https://tiktok.com/@vortisrd) • [Kick](https://kick.com/vortisrd) • [Twitter / X](https://twitter.com/vortisrd)
|
||||
🔗 [GitHub](https://github.com/vortisrd) • [Twitch](https://twitch.tv/vortisrd) • [YouTube](https://youtube.com/@vortisrd) • [Kick](https://kick.com/vortisrd) • [TikTok](https://tiktok.com/@vortisrd) • [Twitter / X](https://twitter.com/vortisrd)
|
||||
|
||||
Heavily inspired by [Nutty](https://nutty.gg)
|
||||
Heavily inspired by [Nutty](https://nutty.gg). *Seriously, go give him some money!*
|
||||
|
||||
🔗 [GitHub](https://github.com/nuttylmao) • [Twitch](https://twitch.tv/nutty) • [YouTube](https://youtube.com/@nuttylmao) • [TikTok](https://tiktok.com/@nuttylmao) • [Twitter / X](https://x.com/nuttylmao)
|
||||
🔗 [GitHub](https://github.com/nuttylmao) • [Twitch](https://twitch.tv/nutty) • [YouTube](https://youtube.com/@nuttylmao) • [Kick](https://kick.com/nutty) • [TikTok](https://tiktok.com/@nuttylmao) • [Twitter / X](https://x.com/nuttylmao)
|
168
chat.html
@@ -5,44 +5,21 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ChatRD</title>
|
||||
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-notify@1.0.4/dist/simple-notify.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap">
|
||||
|
||||
<link rel="stylesheet" href="css/app.css?v=0.0.1">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.0.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-notify@1.0.4/dist/simple-notify.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/animate.css@4.1.1/animate.min.css">
|
||||
|
||||
<link rel="stylesheet" href="css/chatrd.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="statistics">
|
||||
<div class="platform" id="twitch">
|
||||
<i class="fa-brands fa-twitch"></i>
|
||||
<span class="viewers"><i class="fa-solid fa-user"></i> <span>0</span></span>
|
||||
<div id="statistics" style="display: none;">
|
||||
</div>
|
||||
|
||||
<div class="platform" id="youtube">
|
||||
<i class="fa-brands fa-youtube"></i>
|
||||
<span class="viewers"><i class="fa-solid fa-user"></i> <span>0</span></span>
|
||||
<span class="likes"><i class="fa-solid fa-thumbs-up"></i> <span>0</span></span>
|
||||
</div>
|
||||
|
||||
<div class="platform" id="tiktok">
|
||||
<i class="fa-brands fa-tiktok"></i>
|
||||
<span class="viewers"><i class="fa-solid fa-user"></i> <span>0</span></span>
|
||||
<span class="likes"><i class="fa-solid fa-heart"></i> <span>0</span></span>
|
||||
</div>
|
||||
|
||||
<div class="platform" id="kick">
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 800 800">
|
||||
<polygon class="st0" points="336 111 336 239 401 239 401 175 465 175 465 111 657 111 657 303 593 303 593 367 529 367 529 432 593 432 593 496 657 496 657 689 465 689 465 624 401 624 401 560 336 560 336 689 143 689 143 111 336 111"/>
|
||||
</svg>
|
||||
|
||||
<span class="viewers"><i class="fa-solid fa-user"></i> <span>0</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div id="container">
|
||||
<div class="wrapper">
|
||||
@@ -51,64 +28,105 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div id="chat-input">
|
||||
<div id="chat-input" style="display: none;">
|
||||
<div id="chat-autocomplete-list"></div>
|
||||
|
||||
<div class="settings">
|
||||
<div class="chat-enabler" id="twitch">
|
||||
<img src="images/logo-twitch.svg" alt="">
|
||||
<label class="switch"><input type="checkbox" name="chatOnTwitch" checked><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="chat-enabler" id="youtube">
|
||||
<img src="images/logo-youtube.svg" alt="">
|
||||
<label class="switch"><input type="checkbox" name="chatOnYouTube" checked><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="chat-enabler" id="tiktok">
|
||||
<img src="images/logo-tiktok.svg" alt="">
|
||||
<label class="switch"><input type="checkbox" name="chatOnTiktok"><span class="slider"></span></label>
|
||||
<span class="hint"><a title="Chat only. Needs setup. Click here to know how." href="https://github.com/vortisrd/chatrd/tree/main?tab=readme-ov-file#tiktok-chat-setup" target="_blank"><i class="fa-solid fa-circle-info"></i></a></span>
|
||||
</div>
|
||||
<div class="chat-enabler" id="kick">
|
||||
<img src="images/logo-kick.svg" alt="">
|
||||
<label class="switch"><input type="checkbox" name="chatOnKick"><span class="slider"></span></label>
|
||||
<span class="hint"><a title="Chat only. Needs setup. Click here to know how." href="https://github.com/vortisrd/chatrd/tree/main?tab=readme-ov-file#kickbot-installation-on-streamerbot" target="_blank"><i class="fa-solid fa-circle-info"></i></a></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form>
|
||||
<input type="text">
|
||||
<input id="chat-input-text-field" type="text">
|
||||
</form>
|
||||
|
||||
<button id="chat-input-send"><i class="fa-solid fa-paper-plane"></i></button>
|
||||
<button id="chat-input-settings"><i class="fa-solid fa-gear"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.5/dist/purify.min.js"></script>
|
||||
|
||||
|
||||
|
||||
|
||||
<template id="chat-message">
|
||||
|
||||
<div id="" data-user="" class="item">
|
||||
|
||||
<div class="message">
|
||||
<div class="first-message">✨ First-time Chatter</div>
|
||||
<div class="shared-chat"><span class="origin"><i class="fa-solid fa-comments"></i> <strong></strong></span></div>
|
||||
<div class="header"></div>
|
||||
|
||||
<div class="info">
|
||||
<span class="timestamp"></span>
|
||||
<span class="platform"></span>
|
||||
<span class="badges"></span>
|
||||
<span class="avatar"></span>
|
||||
<span class="pronouns"></span>
|
||||
<span class="user"></span>
|
||||
</div>
|
||||
|
||||
<div class="reply"><i class="fa-solid fa-arrow-turn-up"></i> </div>
|
||||
|
||||
<div class="actual-message"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<template id="event-message">
|
||||
|
||||
<div id="" data-user="" class="event">
|
||||
|
||||
<div class="message">
|
||||
<div class="header"></div>
|
||||
<div class="info">
|
||||
<span class="platform"></span>
|
||||
<span class="user"></span>
|
||||
<span class="action"></span>
|
||||
<span class="value"></span>
|
||||
</div>
|
||||
<div class="actual-message"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/simple-notify@1.0.4/dist/simple-notify.min.js"></script>
|
||||
<script src="https://unpkg.com/@streamerbot/client/dist/streamerbot-client.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@streamerbot/client@1.12.1/dist/streamerbot-client.min.js"></script>
|
||||
|
||||
<script src="js/lang/ptbr.js"></script>
|
||||
<script src="js/lang/en.js"></script>
|
||||
<script src="js/lang/es.js"></script>
|
||||
<script src="js/speakerbot.js"></script>
|
||||
<script src="js/sb.js"></script>
|
||||
<script src="js/chatrd.js"></script>
|
||||
|
||||
<script src="js/lang/lang.js"></script>
|
||||
<link href="js/modules/twitch/module.css" rel="stylesheet">
|
||||
<script src="js/modules/twitch/module.js"></script>
|
||||
|
||||
<script src="js/app-mockup.js?v=0.0.4"></script>
|
||||
<script src="js/app.js?v=0.0.4"></script>
|
||||
<link href="js/modules/youtube/module.css" rel="stylesheet">
|
||||
<script src="js/modules/youtube/module.js"></script>
|
||||
|
||||
<script src="js/twitch/module.js?v=0.0.4"></script>
|
||||
<script src="js/youtube/module.js"></script>
|
||||
<script src="js/tiktok/module.js"></script>
|
||||
<script src="js/kick/module.js"></script>
|
||||
<script src="js/patreon/module.js"></script>
|
||||
<script src="js/tipeeestream/module.js"></script>
|
||||
<script src="js/kofi/module.js"></script>
|
||||
<script src="js/fourthwall/module.js"></script>
|
||||
<link href="js/modules/tiktok/module.css" rel="stylesheet">
|
||||
<script src="js/modules/tiktok/module.js"></script>
|
||||
|
||||
<script src="js/streamlabs/module.js"></script>
|
||||
<script src="js/streamelements/module.js"></script>
|
||||
<link href="js/modules/kick/module.css" rel="stylesheet">
|
||||
<script src="https://js.pusher.com/8.4.0/pusher.min.js"></script>
|
||||
<script src="js/modules/kick/module.js"></script>
|
||||
|
||||
<link href="js/modules/streamelements/module.css" rel="stylesheet">
|
||||
<script src="js/modules/streamelements/module.js"></script>
|
||||
|
||||
<link href="js/modules/streamlabs/module.css" rel="stylesheet">
|
||||
<script src="js/modules/streamlabs/module.js"></script>
|
||||
|
||||
<link href="js/modules/patreon/module.css" rel="stylesheet">
|
||||
<script src="js/modules/patreon/module.js"></script>
|
||||
|
||||
<link href="js/modules/tipeeestream/module.css" rel="stylesheet">
|
||||
<script src="js/modules/tipeeestream/module.js"></script>
|
||||
|
||||
<link href="js/modules/kofi/module.css" rel="stylesheet">
|
||||
<script src="js/modules/kofi/module.js"></script>
|
||||
|
||||
<link href="js/modules/fourthwall/module.css" rel="stylesheet">
|
||||
<script src="js/modules/fourthwall/module.js"></script>
|
||||
</body>
|
||||
</html>
|
731
css/chatrd.css
Normal file
@@ -0,0 +1,731 @@
|
||||
/* Basic reset and styling */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sn-notify-outline { --sn-notify-background-color: #27272a; }
|
||||
.sn-notify-outline .sn-notify-title { --sn-notify-title-color: #FFFFFF; }
|
||||
.sn-notify-outline .sn-notify-text { --sn-notify-text-color: rgba(255,255,255,0.5); }
|
||||
|
||||
.sn-notify-outline { --sn-notify-background-color: #27272a; }
|
||||
.sn-notify-outline .sn-notify-title { --sn-notify-title-color: #FFFFFF; }
|
||||
.sn-notify-outline .sn-notify-text { --sn-notify-text-color: rgba(255,255,255,0.5); }
|
||||
|
||||
/* Full-page container styling */
|
||||
html {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
scroll-behavior: smooth;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
font-family: "DM Sans", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
#container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#container .wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#chat.noscrollbar {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#chat::-webkit-scrollbar { width: 8px; }
|
||||
#chat::-webkit-scrollbar-track { background: #1e1e1e; }
|
||||
#chat::-webkit-scrollbar-thumb { background-color: #555; border-radius: 4px; border: 2px solid #1e1e1e; }
|
||||
#chat::-webkit-scrollbar-thumb:hover { background-color: #777; }
|
||||
|
||||
#chat {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
overflow-y: auto;
|
||||
padding: 0px 10px;
|
||||
scrollbar-color: #555 #1e1e1e;
|
||||
}
|
||||
|
||||
#chat.noscrollbar {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#chat .item {
|
||||
position: relative;
|
||||
color: #FFF;
|
||||
font-size: 18px;
|
||||
line-height: 150%;
|
||||
text-shadow: 2px 2px 5px rgba(0,0,0,0.5);
|
||||
transition: all ease-in-out 300ms;
|
||||
/*margin: 5px 0;*/
|
||||
}
|
||||
|
||||
#chat .item .message {
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
transition: all ease-in-out 300ms;
|
||||
}
|
||||
|
||||
#chat .item .info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#chat .item img {
|
||||
height: 22px;
|
||||
filter: drop-shadow(2px 2px 5px rgba(0,0,0,0.25));
|
||||
}
|
||||
|
||||
#chat .item .platform,
|
||||
#chat .item .badges {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
#chat .item .platform .hidden-platform {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 100px;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
#chat .item .badges img {
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
#chat .item .info .avatar {
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
#chat .item .info .avatar img {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
|
||||
#chat .item .timestamp,
|
||||
#chat .item .pronouns {
|
||||
display: inline-block;
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 0px 6px;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#chat .item .pronouns em {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
#chat .item .first-message {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#chat .item .header span {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
#chat .item .reply {
|
||||
max-width: 100%;
|
||||
flex-basis: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#chat .item .reply i {
|
||||
transform: rotate(90deg);
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
#chat .item .shared-chat {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#chat .item .shared-chat span.origin {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 0px 10px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#chat .item .shared-chat img {
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
#chat .item.first-chatter .message,
|
||||
#chat .item.announcement .message {
|
||||
display: inline-block;
|
||||
background: rgba(255,255,255,0.2);
|
||||
outline: 1px solid rgba(255,255,255,0.1);
|
||||
outline-offset: -1px;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
#chat .item .actual-message {
|
||||
margin-top: 3px;
|
||||
font-weight: 300;
|
||||
}
|
||||
#chat .item .actual-message img {
|
||||
vertical-align: middle;
|
||||
height: 28px;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
|
||||
#chat .event {
|
||||
position: relative;
|
||||
color: #FFF;
|
||||
font-size: 18px;
|
||||
line-height: 100%;
|
||||
text-shadow: 2px 2px 5px rgba(0,0,0,0.5);
|
||||
transition: all ease-in-out 300ms;
|
||||
margin: 5px 10px;
|
||||
}
|
||||
|
||||
#chat .event .actual-message {
|
||||
line-height: 150%;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
|
||||
#chat .event .message {
|
||||
display: inline-block;
|
||||
background: rgba(255,255,255,0.2);
|
||||
outline: 1px solid rgba(255,255,255,0.1);
|
||||
outline-offset: -1px;
|
||||
padding: 10px 20px 10px 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
#chat .event img {
|
||||
height: 22px;
|
||||
filter: drop-shadow(2px 2px 5px rgba(0,0,0,0.25));
|
||||
}
|
||||
|
||||
#chat .event .platform {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
#chat .event .info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0 5px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#statistics {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
z-index: 11;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
padding: 10px 25px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px 5px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
#statistics .platform {
|
||||
background: #000;
|
||||
|
||||
outline: 1px solid rgba(255,255,255,0.25);
|
||||
outline-offset: -1px;
|
||||
|
||||
color: #FFF;
|
||||
padding: 10px 15px 10px 10px;
|
||||
border-radius: 5px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 5px;
|
||||
|
||||
box-shadow: 2px 2px 5px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
#statistics .platform img {
|
||||
width: 18px;
|
||||
filter: drop-shadow(2px 2px 5px rgba(0,0,0,0.5));
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------- */
|
||||
/* ---- HORIZONTAL LAYOUT ---- */
|
||||
/* --------------------------- */
|
||||
|
||||
#chat.horizontal {
|
||||
flex-direction: row-reverse;
|
||||
align-items: flex-end;
|
||||
gap: 5px;
|
||||
padding: 0px 20px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: unset;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#chat.horizontal .item {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
#chat.horizontal .item .message {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
|
||||
#chat.horizontal .item .first-message::after,
|
||||
#chat.horizontal .item .shared-chat::after {
|
||||
content: "";
|
||||
display: block;
|
||||
}
|
||||
|
||||
#chat.horizontal .item .shared-chat::after {
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
#chat.horizontal .item .first-message,
|
||||
#chat.horizontal .item .shared-chat,
|
||||
#chat.horizontal .item .header,
|
||||
#chat.horizontal .item .info,
|
||||
#chat.horizontal .item .actual-message {
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
#chat.horizontal .item .reply {
|
||||
display: inline-block;
|
||||
max-width: 300px;
|
||||
transform: translateY(8px);
|
||||
padding: 0 3px 0 0;
|
||||
}
|
||||
|
||||
#chat.horizontal .item .info .timestamp {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
#chat.horizontal .item .info .platform,
|
||||
#chat.horizontal .item .info .badges {
|
||||
display: inline-block !important;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
#chat.horizontal .item .avatar {
|
||||
display: inline-block;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
#chat.horizontal .item .info .pronouns {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
#chat.horizontal .event .info {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#chat.horizontal .event .info .user,
|
||||
#chat.horizontal .event .info .action,
|
||||
#chat.horizontal .event .info .value {
|
||||
display: inline-block;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
#chat.horizontal .item .platform .hidden-platform {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
#chat.horizontal .item.announcement .message {
|
||||
padding: 10px 15px 0 15px;
|
||||
}
|
||||
|
||||
#chat.horizontal .item.announcement .message .info,
|
||||
#chat.horizontal .item.announcement .message .actual-message {
|
||||
display: inline-block !important;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* --------------------------- */
|
||||
/* ----- ONE LINE LAYOUT ----- */
|
||||
/* --------------------------- */
|
||||
|
||||
#chat.oneline {
|
||||
white-space: wrap;
|
||||
}
|
||||
|
||||
#chat.oneline .item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#chat.oneline .item .message {
|
||||
padding: 1px 10px;
|
||||
}
|
||||
|
||||
#chat.oneline .item .first-message::after,
|
||||
#chat.oneline .item .shared-chat::after {
|
||||
content: "";
|
||||
display: block;
|
||||
}
|
||||
|
||||
#chat.oneline .item .shared-chat::after {
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
#chat.oneline .item .first-message,
|
||||
#chat.oneline .item .shared-chat,
|
||||
#chat.oneline .item .info,
|
||||
#chat.oneline .item .actual-message {
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
#chat.oneline .item .info .timestamp {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
#chat.oneline .item .info .platform,
|
||||
#chat.oneline .item .info .badges {
|
||||
display: inline-block !important;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
#chat.oneline .item .platform .hidden-platform {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
#chat.oneline .item .avatar {
|
||||
display: inline-block;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
#chat.oneline .item .info .pronouns {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
#chat.oneline .event .info {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#chat.oneline .event .info .user,
|
||||
#chat.oneline .event .info .action,
|
||||
#chat.oneline .event .info .value {
|
||||
display: inline-block;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* --------------------------- */
|
||||
/* ---- CHAT INPUT LAYOUT ---- */
|
||||
/* --------------------------- */
|
||||
|
||||
#chat-input {
|
||||
padding: 15px 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#chat-input.enabled {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#chat-input input[type=text] {
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
background: #222;
|
||||
color: #FFF;
|
||||
padding: 10px 50px 10px 15px;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
transition: all ease-in-out 300ms;
|
||||
}
|
||||
|
||||
#chat-input button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #FFF;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#chat-input button.active,
|
||||
#chat-input button:hover {
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
#chat-input #chat-input-send {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
#chat-input .settings {
|
||||
display: none;
|
||||
background: #0c0c0c;
|
||||
padding: 5px 10px;
|
||||
border-radius: 100px;
|
||||
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: 0px;
|
||||
z-index: 11;
|
||||
|
||||
box-shadow: 0 0 10px rgba(255,255,255,0.05)
|
||||
}
|
||||
|
||||
#chat-input .settings.active {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#chat-input .settings::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -10px; /* posiciona fora do balão */
|
||||
right: 25px; /* onde a pontinha aparece horizontalmente */
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid #0c0c0c; /* mesma cor do balão */
|
||||
}
|
||||
|
||||
#chat-input .settings img {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
|
||||
#chat-input .chat-enabler {
|
||||
display: inline-flex;
|
||||
padding: 5px;
|
||||
gap: 5px;
|
||||
color: #FFF;
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#chat-input .settings .switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#chat-input .settings .hint {
|
||||
color: #FFF;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#chat-input .settings .hint a {
|
||||
color: #ffcc00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
#chat-input .settings .switch input[type=checkbox] {
|
||||
opacity: 1;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
#chat-input .settings .slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #CCC;
|
||||
transition: .4s;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
#chat-input .settings .slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 16px;
|
||||
left: 3px;
|
||||
top: 2px;
|
||||
bottom: 0;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
#chat-input .settings input[type=checkbox]:checked + .slider:before {
|
||||
transform: translateX(19px);
|
||||
}
|
||||
|
||||
#chat-input #twitch input[type=checkbox]:checked + .slider {
|
||||
background-color: #a970ff;
|
||||
}
|
||||
#chat-input #youtube input[type=checkbox]:checked + .slider {
|
||||
background-color: #FF0000;
|
||||
}
|
||||
#chat-input #tiktok input[type=checkbox]:checked + .slider {
|
||||
background-color: #ff0050;
|
||||
}
|
||||
#chat-input #kick input[type=checkbox]:checked + .slider {
|
||||
background-color: #48d415;
|
||||
}
|
||||
|
||||
|
||||
#chat-input .settings input[type=checkbox]:checked + .slider {
|
||||
background-color: #03c4de;
|
||||
}
|
||||
#chat-input .setting input[type=checkbox]:disabled + .slider {
|
||||
background-color: #000 !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#chat-autocomplete-list {
|
||||
position: absolute;
|
||||
bottom: 65px;
|
||||
left: 10px;
|
||||
width: calc(100% - 40px);
|
||||
border-radius: 5px;
|
||||
background: #222;
|
||||
color: #FFF;
|
||||
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
overflow-y: visible;
|
||||
|
||||
font-size: 14px;;
|
||||
|
||||
color: #696969;
|
||||
}
|
||||
|
||||
#chat-autocomplete-list::-webkit-scrollbar { width: 10px; }
|
||||
#chat-autocomplete-list::-webkit-scrollbar-track { background: #1e1e1e; }
|
||||
#chat-autocomplete-list::-webkit-scrollbar-thumb { background-color: #555; border-radius: 10px; border: 2px solid #1e1e1e; }
|
||||
#chat-autocomplete-list::-webkit-scrollbar-thumb:hover { background-color: #777; }
|
||||
|
||||
#chat-autocomplete-list div {
|
||||
margin: 5px 10px;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#chat-autocomplete-list div.autocomplete-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#chat-autocomplete-list div:not(.autocomplete-item) {
|
||||
background: #191919;
|
||||
color: #FFF;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#chat-autocomplete-list .autocomplete-item strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#chat-autocomplete-list .autocomplete-item:hover,
|
||||
#chat-autocomplete-list .autocomplete-item.active {
|
||||
color: #FFF;
|
||||
background: #292929;
|
||||
}
|
||||
|
||||
#chat-autocomplete-list .autocomplete-item small {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.item.chat .chatmoderation {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
transition: all ease-in-out 300ms;
|
||||
}
|
||||
|
||||
.item.chat:hover .message {
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
.item.chat:hover .chatmoderation {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.item.chat .chatmoderation button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #FFF;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
transition: all ease-in-out 300ms;
|
||||
margin: 0 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item.chat .chatmoderation button:hover {
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
|
||||
.item.chat.twitch.streamer .chatmoderation button:nth-child(2),
|
||||
.item.chat.twitch.streamer .chatmoderation button:nth-child(3),
|
||||
.item.chat.kick.streamer .chatmoderation button:nth-child(1),
|
||||
.item.chat.kick.streamer .chatmoderation button:nth-child(2),
|
||||
.item.chat.youtube.owner .chatmoderation {
|
||||
display: none;
|
||||
}
|
715
css/settings.css
@@ -1,109 +1,316 @@
|
||||
/* Basic reset and styling */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
/* Full-page container styling */
|
||||
html {
|
||||
width: 100vw;
|
||||
scroll-behavior: smooth;
|
||||
overflow-wrap: break-word;
|
||||
background-color: #121212;
|
||||
color: #FFF;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a { color: #ffcc00; }
|
||||
body {
|
||||
font-family: "DM Sans", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
background-color: #121212;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
a { color: #DBB048; }
|
||||
|
||||
hr {
|
||||
border: 1px solid #222;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
padding: 15px 30px;
|
||||
border-bottom: 1px solid #222;
|
||||
margin: 15px 0 0 0;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
#container {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
h2 i {
|
||||
margin-right: 5px;
|
||||
#container .wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow-x: hidden;
|
||||
width: 100vw;
|
||||
/*height: 100vh;*/
|
||||
}
|
||||
|
||||
h2 svg {
|
||||
width: 26px;
|
||||
vertical-align: bottom;
|
||||
fill: #FFF;
|
||||
}
|
||||
|
||||
h2 img {
|
||||
width: 26px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
h2 button {
|
||||
display: none;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
|
||||
border: none;
|
||||
background: black;
|
||||
text-align: center;
|
||||
#settings {
|
||||
width: 640px;
|
||||
color: #FFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
#settings form {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#settings .title {
|
||||
text-align: center;
|
||||
margin: 20px 0 0 0;
|
||||
}
|
||||
|
||||
#settings .title h1 {
|
||||
background-image: url('../images/logo-chatrd.png');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
text-indent: -9999px;
|
||||
width: 200px;
|
||||
height: 78px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#settings .platform {
|
||||
background: #191919;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
outline: 1px solid rgba(255,255,255,0.05);
|
||||
outline-offset: -1px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
#settings .platform h2 {
|
||||
display: inline-flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#settings .platform h2 img {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
#settings .platform .platform-enable {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#settings .config {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0px;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#settings .config strong {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
#settings .config small {
|
||||
color: #717171;
|
||||
}
|
||||
|
||||
#settings .config i.fa-solid.fa-arrow-turn-up {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
#settings .config input[type=color] {
|
||||
background: #111;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
|
||||
#settings .config i {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#settings .config h2 i {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
|
||||
#settings .switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 27px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#settings .switch input[type=checkbox] {
|
||||
opacity: 1;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#settings .config input[type=text],
|
||||
#settings .config input[type=number],
|
||||
#settings .config textarea {
|
||||
font-family: "Inter", sans-serif;
|
||||
border: none;
|
||||
background:#222;
|
||||
color: #FFF;
|
||||
padding: 10px 15px;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#settings .switch .slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #CCC;
|
||||
transition: .4s;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
#settings .switch .slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 1.3em;
|
||||
width: 1.2em;
|
||||
border-radius: 16px;
|
||||
left: 5px;
|
||||
top: 3px;
|
||||
bottom: 0;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
#settings .switch input[type=checkbox]:checked + .slider:before {
|
||||
transform: translateX(1.4em);
|
||||
}
|
||||
|
||||
#settings .switch input[type=checkbox]:checked + .slider {
|
||||
background-color: #DBB048;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#settings .config .emote-list {
|
||||
width:100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* allow items to wrap */
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#settings .config .emote-list .emote-item {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: #222;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#settings .config .emote-list .emote-item button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
transition: .4s;
|
||||
|
||||
text-align: center;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#settings .config .emote-list .emote-item button.add {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
right: unset;
|
||||
|
||||
transform: translate(-50%,-50%);
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
#settings .config .emote-list .emote-item button.add:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
#settings .config .emote-list .emote-item button.add i {
|
||||
vertical-align: middle;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
|
||||
#settings .config .emote-list .emote-item em {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
#settings .config .emote-list .emote-item img {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
|
||||
#settings small.warning {
|
||||
display: inline-block;
|
||||
padding: 10px 20px 10px 10px;
|
||||
background-color: #232323;
|
||||
color: #FFF;
|
||||
border-radius: 10px;
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
#settings small.warning i {
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
|
||||
#speakerbot .switch input[type=checkbox]:checked + .slider { background-color: #00dbff; }
|
||||
#twitch .switch input[type=checkbox]:checked + .slider { background-color: #a970ff; }
|
||||
#youtube .switch input[type=checkbox]:checked + .slider { background-color: #FF0000; }
|
||||
#tiktok .switch input[type=checkbox]:checked + .slider { background-color: #ff0050; }
|
||||
#kick .switch input[type=checkbox]:checked + .slider { background-color: #48d415; }
|
||||
#streamelements .switch input[type=checkbox]:checked + .slider { background-color: #2700ff; }
|
||||
#streamlabs .switch input[type=checkbox]:checked + .slider { background-color: #80f5d2; }
|
||||
#patreon .switch input[type=checkbox]:checked + .slider { background-color: #ff5900; }
|
||||
#tipeee .switch input[type=checkbox]:checked + .slider { background-color: #e02f44; }
|
||||
#kofi .switch input[type=checkbox]:checked + .slider { background-color: #72a5f2; }
|
||||
#fourthwall .switch input[type=checkbox]:checked + .slider { background-color: #0042ff; }
|
||||
|
||||
.switch input[type=checkbox]:disabled + .slider { background-color: #111 !important; }
|
||||
|
||||
|
||||
|
||||
#font-value,
|
||||
#bg-opacity-value {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
background: #121212;
|
||||
text-align: center;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
h2 button i {
|
||||
margin: 0;
|
||||
transition: transform 0.3s ease;
|
||||
#preview {
|
||||
width: calc(100% - 640px);
|
||||
height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.accordion-container {
|
||||
/*max-height: 0;
|
||||
overflow: hidden;*/
|
||||
transition: max-height 0.4s ease;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#chat-divided {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 30px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
#chat-settings { width: 640px; }
|
||||
#chat-preview { width: calc(100% - 640px); }
|
||||
|
||||
#chat-settings h1 {
|
||||
width: 200px;
|
||||
height: 80px;
|
||||
background: url('../images/logo-chatrd.png') center center no-repeat;
|
||||
background-size: cover;
|
||||
margin: auto;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
#chat-preview iframe {
|
||||
position: sticky;
|
||||
top: 30px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100vh - 60px);
|
||||
#preview iframe {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 0;
|
||||
width: calc(100% - 640px);
|
||||
height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.url-bar {
|
||||
@@ -135,300 +342,52 @@ h2 button i {
|
||||
background: #292929;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
border-radius: 0 0 20px 20px;
|
||||
background: #171717;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
margin: 0 0 30px 0;
|
||||
}
|
||||
|
||||
.tab-content .wrapper {
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
/* When visible */
|
||||
.tab-content.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tab-content .setting {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0px;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab-content .setting.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-content .setting.column label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-content .setting.column .emote-list {
|
||||
width:100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* allow items to wrap */
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.tab-content .setting.column .emote-list .emote-item {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: #222;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.tab-content .setting.column .emote-list .emote-item button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
transition: .4s;
|
||||
|
||||
text-align: center;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab-content .setting.column .emote-list .emote-item button.add {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
right: unset;
|
||||
|
||||
transform: translate(-50%,-50%);
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.tab-content .setting.column .emote-list .emote-item button.add:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.tab-content .setting.column .emote-list .emote-item button.add i {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
.tab-content .setting.column .emote-list .emote-item em {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.tab-content .setting.column .emote-list .emote-item img {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
|
||||
.tab-content .setting i {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.tab-content .setting i.fa-arrow-turn-up {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
|
||||
.tab-content .setting small { color: #777; }
|
||||
|
||||
.tab-content .setting input[type=text],
|
||||
.tab-content .setting input[type=number],
|
||||
.tab-content .setting textarea {
|
||||
font-family: "Inter", sans-serif;
|
||||
border: none;
|
||||
background:#222;
|
||||
color: #FFF;
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tab-content .setting textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.tab-content .setting.select select {
|
||||
font-family: "Inter", sans-serif;
|
||||
border: none;
|
||||
background:#222;
|
||||
color: #FFF;
|
||||
padding: 10px 20px 10px 10px;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tab-content .setting .switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
.tab-content .setting .switch input[type=checkbox] {
|
||||
opacity: 1;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.tab-content .setting .slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #CCC;
|
||||
transition: .4s;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.tab-content .setting .slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 1.3em;
|
||||
width: 1.2em;
|
||||
border-radius: 16px;
|
||||
left: 5px;
|
||||
top: 3px;
|
||||
bottom: 0;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tab-content .setting input[type=color] {
|
||||
background: #111;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.tab-content .setting input[type=checkbox]:checked + .slider {
|
||||
background-color: #03c4de;
|
||||
}
|
||||
|
||||
.tab-content .setting input[type=checkbox]:checked + .slider:before {
|
||||
transform: translateX(1.4em);
|
||||
}
|
||||
|
||||
.tab-content#twitch .setting input[type=checkbox]:checked + .slider {
|
||||
background-color: #a970ff;
|
||||
}
|
||||
.tab-content#youtube .setting input[type=checkbox]:checked + .slider {
|
||||
background-color: #FF0000;
|
||||
}
|
||||
.tab-content#tiktok .setting input[type=checkbox]:checked + .slider {
|
||||
background-color: #ff0050;
|
||||
}
|
||||
|
||||
.tab-content#kick .setting input[type=checkbox]:checked + .slider {
|
||||
background-color: #48d415;
|
||||
}
|
||||
|
||||
.tab-content#extras .setting input[type=checkbox]:checked + .slider {
|
||||
background-color: #00dd63;
|
||||
}
|
||||
|
||||
.tab-content#patreon .setting input[type=checkbox]:checked + .slider {
|
||||
background-color: #ff5900;
|
||||
}
|
||||
|
||||
.tab-content#tipeee .setting input[type=checkbox]:checked + .slider {
|
||||
background-color: #e02f44;
|
||||
}
|
||||
|
||||
.tab-content#kofi .setting input[type=checkbox]:checked + .slider {
|
||||
background-color: #72a5f2;
|
||||
}
|
||||
|
||||
.tab-content#fourthwall .setting input[type=checkbox]:checked + .slider {
|
||||
background-color: #0042ff;
|
||||
}
|
||||
|
||||
.tab-content .setting input[type=checkbox]:disabled + .slider {
|
||||
background-color: #000 !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.slider-value {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
background: #121212;
|
||||
text-align: center;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
|
||||
footer {
|
||||
width: 640px;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 20px;
|
||||
background: rgba(18,18,18,0.5);
|
||||
backdrop-filter: blur(10px);
|
||||
margin-top: 80px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
footer a {
|
||||
display: inline-block;
|
||||
margin: 10px 5px;
|
||||
font-size: 20px;
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
footer a svg {
|
||||
width: 24px;
|
||||
vertical-align: bottom;
|
||||
fill: #ffcc00;
|
||||
color: #DBB048;
|
||||
}
|
||||
|
||||
footer a img {
|
||||
width: 24px;
|
||||
height: 22px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
|
||||
footer a[href="https://kick.com/vortisrd"] svg {
|
||||
height: 26px;
|
||||
vertical-align: bottom;
|
||||
fill: #DBB048;
|
||||
}
|
||||
|
||||
footer .nav-bar {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
footer .nav-bar a {
|
||||
display: inline-block;
|
||||
border-radius: 100px;
|
||||
padding: 10px 15px;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
@@ -437,58 +396,14 @@ footer .nav-bar a {
|
||||
}
|
||||
|
||||
footer .nav-bar a:hover {
|
||||
color: #ffcc00;
|
||||
color: #DBB048;
|
||||
background: rgba(0,0,0,0.40);
|
||||
}
|
||||
|
||||
footer .nav-bar a svg {
|
||||
width: 20px;
|
||||
fill: #FFF;
|
||||
transition: all ease-in-out 300ms;
|
||||
footer .nav-bar a img {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
footer .nav-bar a:hover svg {
|
||||
fill: #ffcc00;
|
||||
}
|
||||
|
||||
|
||||
footer .nav-bar a.active {
|
||||
color: #ffcc00;
|
||||
background: rgba(0,0,0,0.20);
|
||||
}
|
||||
|
||||
footer .nav-bar a.active svg {
|
||||
fill: #ffcc00;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
|
||||
#chat-divided {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#chat-settings {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#chat-settings .field {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#chat-settings .field input {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
#chat-preview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
footer { font-size: 12px; }
|
||||
}
|
||||
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
@@ -498,6 +413,11 @@ footer .nav-bar a.active svg {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.modal small {
|
||||
color: #717171;
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
@@ -570,11 +490,18 @@ footer .nav-bar a.active svg {
|
||||
}
|
||||
|
||||
|
||||
#memberemotesbstatus.online i {
|
||||
color: #00dd63;
|
||||
text-shadow: 0 0 5px #00dd63;
|
||||
#settings .config #streamerBotStatus label i,
|
||||
#settings .config #speakerBotStatus label i,
|
||||
#settings .config #tikfinityStatus label i,
|
||||
#settings .config #kickStatus label i {
|
||||
color: #ce2c2c;
|
||||
text-shadow: 0 0 5px #ce2c2c;
|
||||
}
|
||||
#memberemotesbstatus.offline i {
|
||||
color: #ff0000;
|
||||
text-shadow: 0 0 5px #ff0000;
|
||||
|
||||
#settings .config #streamerBotStatus.connected label i,
|
||||
#settings .config #speakerBotStatus.connected label i,
|
||||
#settings .config #tikfinityStatus.connected label i,
|
||||
#settings .config #kickStatus.connected label i {
|
||||
color: #14c22b;
|
||||
text-shadow: 0 0 5px #14c22b;
|
||||
}
|
BIN
images/img-empty.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
images/logo-speakerbot.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
images/logo-streamerbot.png
Normal file
After Width: | Height: | Size: 30 KiB |
1051
index.html
680
js/chatrd.js
Normal file
@@ -0,0 +1,680 @@
|
||||
/* ----------------------- */
|
||||
/* OPTIONS */
|
||||
/* ----------------------- */
|
||||
|
||||
const showPlatform = getURLParam("showPlatform", true);
|
||||
const showPlatformDot = getURLParam("showPlatformDot", false);
|
||||
const showAvatar = getURLParam("showAvatar", true);
|
||||
const showTimestamps = getURLParam("showTimestamps", true);
|
||||
const ampmTimeStamps = getURLParam("ampmTimeStamps", false);
|
||||
const showBadges = getURLParam("showBadges", true);
|
||||
const showPlatformStatistics = getURLParam("showPlatformStatistics", true);
|
||||
|
||||
const chatThreshold = 50;
|
||||
const chatOneLine = getURLParam("chatOneLine", false);
|
||||
const chatHorizontal = getURLParam("chatHorizontal", false);
|
||||
const chatFontSize = getURLParam("chatFontSize", 1);
|
||||
const chatBackground = getURLParam("chatBackground", "#121212");
|
||||
const chatBackgroundOpacity = getURLParam("chatBackgroundOpacity", 1);
|
||||
const chatScrollBar = getURLParam("chatScrollBar", false);
|
||||
const chatField = getURLParam("chatField", false);
|
||||
const chatModeration = getURLParam("chatModeration", false);
|
||||
|
||||
const excludeCommands = getURLParam("excludeCommands", true);
|
||||
const ignoreChatters = getURLParam("ignoreChatters", "");
|
||||
const ignoreUserList = ignoreChatters.split(',').map(item => item.trim().toLowerCase()) || [];
|
||||
|
||||
const hideAfter = getURLParam("hideAfter", 0);
|
||||
|
||||
const chatContainer = document.querySelector('#chat');
|
||||
const chatTemplate = document.querySelector('#chat-message');
|
||||
const eventTemplate = document.querySelector('#event-message');
|
||||
|
||||
const userColors = new Map();
|
||||
|
||||
|
||||
if (showPlatformStatistics == true) { document.querySelector('#statistics').style.display = ''; }
|
||||
if (chatScrollBar == false) { chatContainer.classList.add('noscrollbar'); }
|
||||
if (chatOneLine == true && !chatHorizontal) { chatContainer.classList.add('oneline'); }
|
||||
if (chatHorizontal == true) {
|
||||
chatContainer.classList.remove('oneline');
|
||||
chatContainer.classList.add('horizontal');
|
||||
}
|
||||
|
||||
let backgroundColor = hexToRGBA(chatBackground,chatBackgroundOpacity);
|
||||
chatContainer.parentElement.style.backgroundColor = backgroundColor;
|
||||
|
||||
chatContainer.style.zoom = chatFontSize;
|
||||
|
||||
if (chatField) {
|
||||
const chatfieldelement = document.getElementById("chat-input");
|
||||
chatfieldelement.style.display = '';
|
||||
}
|
||||
|
||||
|
||||
function addMessageItem(platform, clone, classes, userid, messageid) {
|
||||
removeExtraChatMessages();
|
||||
|
||||
let chatmodtwitch = `<div class="chatmoderation">
|
||||
<button onclick="executeModCommand(event, '/deletemessage ${messageid}')" title="Remove Message"><i class="fa-solid fa-trash-can"></i></button>
|
||||
<button onclick="executeModCommand(event, '/timeout ${userid}')" title="Timeout User"><i class="fa-solid fa-stopwatch"></i></button>
|
||||
<button onclick="executeModCommand(event, '/ban ${userid}')" title="Ban User"><i class="fa-solid fa-gavel"></i></button>
|
||||
</div>`;
|
||||
|
||||
let chatmodyoutube = `<div class="chatmoderation">
|
||||
<button onclick="executeModCommand(event, '/yt/timeout ${userid}')" title="Timeout User"><i class="fa-solid fa-stopwatch"></i></button>
|
||||
<button onclick="executeModCommand(event, '/yt/ban ${userid}')" title="Ban User"><i class="fa-solid fa-gavel"></i></button>
|
||||
</div>`;
|
||||
|
||||
let chatmodkick = `<div class="chatmoderation">
|
||||
<button onclick="executeModCommand(event, '/kick/timeout ${userid}')" title="Timeout User"><i class="fa-solid fa-stopwatch"></i></button>
|
||||
<button onclick="executeModCommand(event, '/kick/ban ${userid}')" title="Ban User"><i class="fa-solid fa-gavel"></i></button>
|
||||
</div>`;
|
||||
|
||||
if (showSpeakerbot == true && speakerBotChatRead == true) { speakerBotTTSRead(clone, 'chat'); }
|
||||
|
||||
const root = clone.firstElementChild;
|
||||
root.classList.add(...classes);
|
||||
root.dataset.user = userid;
|
||||
root.id = messageid;
|
||||
root.style.opacity = '0';
|
||||
|
||||
const platformElement = clone.querySelector('.platform');
|
||||
|
||||
if (showPlatform == true) {
|
||||
platformElement.innerHTML = `<img src="js/modules/${platform}/images/logo-${platform}.svg">`;
|
||||
}
|
||||
|
||||
if (showPlatformDot == true) {
|
||||
platformElement.innerHTML = `<span class="hidden-platform ${platform}"></span>`;
|
||||
}
|
||||
|
||||
if (showPlatform == false && showPlatformDot == false) {
|
||||
platformElement.remove();
|
||||
}
|
||||
|
||||
const timestamp = clone.querySelector('.timestamp');
|
||||
if (timestamp) {
|
||||
if (showTimestamps) {
|
||||
timestamp.textContent = whatTimeIsIt();
|
||||
} else {
|
||||
timestamp.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const dimensionProp = chatHorizontal ? 'Width' : 'Height';
|
||||
|
||||
// Starts it collapsed
|
||||
root.style[dimensionProp.toLowerCase()] = '0px';
|
||||
|
||||
if (chatModeration == true) {
|
||||
switch (platform) {
|
||||
case "twitch":
|
||||
root.insertAdjacentHTML("beforeend", chatmodtwitch);
|
||||
break;
|
||||
|
||||
case "youtube":
|
||||
root.insertAdjacentHTML("beforeend", chatmodyoutube);
|
||||
break;
|
||||
|
||||
case "kick":
|
||||
root.insertAdjacentHTML("beforeend", chatmodkick);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Plataforma desconhecida: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
chatContainer.prepend(clone);
|
||||
|
||||
const item = document.getElementById(messageid);
|
||||
const itemDimension = item.querySelector('.message')?.[`offset${dimensionProp}`] || 0;
|
||||
|
||||
// Animates the item
|
||||
requestAnimationFrame(() => {
|
||||
item.style[dimensionProp.toLowerCase()] = itemDimension + 'px';
|
||||
item.style.opacity = '1';
|
||||
});
|
||||
|
||||
item.addEventListener('transitionend', () => {
|
||||
item.style[dimensionProp.toLowerCase()] = '';
|
||||
item.style.opacity = '';
|
||||
}, { once: true });
|
||||
|
||||
// Hides it after a while
|
||||
if (hideAfter > 0) {
|
||||
setTimeout(() => {
|
||||
item.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
item.remove();
|
||||
}, 1000);
|
||||
}, Math.floor(hideAfter * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function addEventItem(platform, clone, classes, userid, messageid) {
|
||||
removeExtraChatMessages();
|
||||
|
||||
if (showSpeakerbot == true && speakerBotEventRead == true) { speakerBotTTSRead(clone, 'event'); }
|
||||
|
||||
const root = clone.firstElementChild;
|
||||
root.classList.add(...classes);
|
||||
root.dataset.user = userid;
|
||||
root.id = messageid;
|
||||
root.style.opacity = '0';
|
||||
|
||||
const platformElement = clone.querySelector('.platform');
|
||||
|
||||
if (showPlatform == true) {
|
||||
platformElement.innerHTML = `<img src="js/modules/${platform}/images/logo-${platform}.svg">`;
|
||||
}
|
||||
|
||||
if (showPlatformDot == true) {
|
||||
platformElement.innerHTML = `<span class="hidden-platform ${platform}"></span>`;
|
||||
}
|
||||
|
||||
if (showPlatform == false && showPlatformDot == false) {
|
||||
platformElement.remove();
|
||||
}
|
||||
|
||||
const timestamp = clone.querySelector('.timestamp');
|
||||
if (timestamp) {
|
||||
if (showTimestamps) {
|
||||
timestamp.textContent = whatTimeIsIt();
|
||||
} else {
|
||||
timestamp.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const dimensionProp = chatHorizontal ? 'Width' : 'Height';
|
||||
|
||||
// Starts it collapsed
|
||||
root.style[dimensionProp.toLowerCase()] = '0px';
|
||||
|
||||
chatContainer.prepend(clone);
|
||||
|
||||
const item = document.getElementById(messageid);
|
||||
const itemDimension = item.querySelector('.message')?.[`offset${dimensionProp}`] || 0;
|
||||
|
||||
// Animates the item
|
||||
requestAnimationFrame(() => {
|
||||
item.style[dimensionProp.toLowerCase()] = itemDimension + 'px';
|
||||
item.style.opacity = '1';
|
||||
});
|
||||
|
||||
item.addEventListener('transitionend', () => {
|
||||
item.style[dimensionProp.toLowerCase()] = '';
|
||||
item.style.opacity = '';
|
||||
}, { once: true });
|
||||
|
||||
// Hides it after a while
|
||||
if (hideAfter > 0) {
|
||||
setTimeout(() => {
|
||||
item.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
item.remove();
|
||||
}, 1000);
|
||||
}, Math.floor(hideAfter * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function removeItem(element) {
|
||||
element.remove();
|
||||
}
|
||||
|
||||
|
||||
function removeExtraChatMessages() {
|
||||
const chatMessages = chatContainer.querySelectorAll(':scope > div');
|
||||
const total = chatMessages.length;
|
||||
|
||||
if (total >= chatThreshold) {
|
||||
const toRemove = Math.floor(total * 0.25); // 25% do total
|
||||
for (let i = 0; i < toRemove; i++) {
|
||||
const last = chatContainer.lastElementChild;
|
||||
if (last) chatContainer.removeChild(last);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function whatTimeIsIt() {
|
||||
const now = new Date();
|
||||
const hours24 = now.getHours();
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
const ampm = hours24 >= 12 ? 'PM' : 'AM';
|
||||
const hours12 = (hours24 % 12) || 12;
|
||||
|
||||
if (ampmTimeStamps === true) {
|
||||
return `${hours12}:${minutes} ${ampm}`;
|
||||
} else {
|
||||
return `${hours24}:${minutes}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to format large numbers (e.g., 1000 => '1K')
|
||||
function formatNumber(num) {
|
||||
if (num >= 1000000) {
|
||||
let numStr = (num / 1000000).toFixed(1);
|
||||
if (numStr.endsWith('.0')) {
|
||||
numStr = numStr.slice(0, -2);
|
||||
}
|
||||
return numStr + 'M';
|
||||
}
|
||||
else if (num >= 1000) {
|
||||
let numStr = (num / 1000).toFixed(1);
|
||||
if (numStr.endsWith('.0')) {
|
||||
numStr = numStr.slice(0, -2);
|
||||
}
|
||||
return numStr + 'K';
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
|
||||
function formatCurrency(amount, currencyCode) {
|
||||
if (!currencyCode) { currencyCode = 'USD'; }
|
||||
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
style: 'currency',
|
||||
currency: currencyCode,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
|
||||
function createRandomString(length) {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function createRandomColor(platform, username) {
|
||||
if (userColors.get(platform).has(username)) {
|
||||
return userColors.get(platform).get(username);
|
||||
}
|
||||
else {
|
||||
const randomColor = "hsl(" + Math.random() * 360 + ", 100%, 75%)";
|
||||
userColors.get(platform).set(username, randomColor);
|
||||
return randomColor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function hexToRGBA(hexadecimal,opacity) {
|
||||
const hex = hexadecimal;
|
||||
const alpha = parseFloat(opacity);
|
||||
|
||||
// Converter hex para RGB
|
||||
const r = parseInt(hex.substr(1, 2), 16);
|
||||
const g = parseInt(hex.substr(3, 2), 16);
|
||||
const b = parseInt(hex.substr(5, 2), 16);
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
|
||||
function stripStringFromHtml(html) {
|
||||
let doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
return doc.body.textContent || "";
|
||||
}
|
||||
|
||||
|
||||
async function cleanStringOfHTMLButEmotes(string) {
|
||||
// Cria um elemento DOM temporário
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = string;
|
||||
|
||||
// Substitui <img class="emote" alt="..."> por texto do alt
|
||||
const emotes = container.querySelectorAll('img.emote[alt]');
|
||||
emotes.forEach(img => {
|
||||
const altText = img.getAttribute('alt');
|
||||
const textNode = document.createTextNode(altText);
|
||||
img.replaceWith(textNode);
|
||||
});
|
||||
|
||||
// Remove todo o restante do HTML
|
||||
return container.textContent || "";
|
||||
}
|
||||
|
||||
|
||||
const pushNotify = (data) => {
|
||||
|
||||
const SimpleNotify = {
|
||||
effect: 'fade',
|
||||
speed: 500,
|
||||
customClass: 'toasty',
|
||||
customIcon: '',
|
||||
showIcon: true,
|
||||
showCloseButton: true,
|
||||
autoclose: true,
|
||||
autotimeout: 5000,
|
||||
notificationsGap: null,
|
||||
notificationsPadding: null,
|
||||
type: 'outline',
|
||||
position: 'x-center bottom',
|
||||
customWrapper: '',
|
||||
};
|
||||
const mergedData = {
|
||||
...SimpleNotify,
|
||||
...data
|
||||
}
|
||||
new Notify (mergedData);
|
||||
|
||||
}
|
||||
|
||||
const notifyError = (err) => {
|
||||
err.status = 'error';
|
||||
pushNotify(err);
|
||||
}
|
||||
|
||||
const notifyInfo = (info) => {
|
||||
info.status = 'info';
|
||||
pushNotify(info);
|
||||
}
|
||||
|
||||
const notifyWarning = (warn) => {
|
||||
warn.status = 'warning';
|
||||
pushNotify(warn);
|
||||
}
|
||||
|
||||
|
||||
const notifySuccess = (success) => {
|
||||
success.status = 'success';
|
||||
pushNotify(success);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* -------------------------- */
|
||||
/* ---- CHAT INPUT UTILS ---- */
|
||||
/* -------------------------- */
|
||||
|
||||
const chatcommandslist = document.getElementById('chat-autocomplete-list');
|
||||
let chatcurrentFocus = -1;
|
||||
|
||||
const chatInputSend = document.getElementById("chat-input-send");
|
||||
const chatInputForm = document.querySelector("#chat-input form");
|
||||
const chatInput = chatInputForm.querySelector("input[type=text]")
|
||||
|
||||
let chatcommands = {
|
||||
"Twitch" : [
|
||||
{ "name" : "/me", "usage" : "Creates a colored message. <b>Usage: /me [message]</b>" },
|
||||
{ "name" : "/clip", "usage" : "Creates a 30s clip. <b>Usage: /clip</b>" },
|
||||
{ "name" : "/announce", "usage" : "Sends an announcement. <b>Usage: /announce [message]</b>" },
|
||||
{ "name" : "/announceblue", "usage" : "Sends an announcement in blue. <b>Usage: /announceblue [message]</b>" },
|
||||
{ "name" : "/announcegreen", "usage" : "Sends an announcement in green. <b>Usage: /announcegreen [message]</b>" },
|
||||
{ "name" : "/announceorange", "usage" : "Sends an announcement in orange. <b>Usage: /announceorange [message]</b>" },
|
||||
{ "name" : "/announcepurple", "usage" : "Sends an announcement in purple. <b>Usage: /announcepurple [message]</b>" },
|
||||
{ "name" : "/clear", "usage" : "Clear Chat Messages. <b>Usage: /clear</b>" },
|
||||
{ "name" : "/slow", "usage" : "Activates slow mode. <b>Usage: /slow [duration]</b>" },
|
||||
{ "name" : "/slowoff", "usage" : "Deactivates slow mode. <b>Usage: /slowoff</b>" },
|
||||
{ "name" : "/subscribers", "usage" : "Activates subscribers only mode. <b>Usage: /subscribers</b>" },
|
||||
{ "name" : "/subscribersoff", "usage" : "Deactivates subscribers only mode. <b>Usage: /subscribersoff</b>" },
|
||||
{ "name" : "/emoteonly", "usage" : "Activates emote only mode. <b>Usage: /emoteonly</b>" },
|
||||
{ "name" : "/emoteonlyoff", "usage" : "Deactivates emote only mode. <b>Usage: /emoteonlyoff</b>" },
|
||||
{ "name" : "/commercial", "usage" : "Add a commercial break. <b>Usage: /commercial [duration]</b>" },
|
||||
{ "name" : "/timeout", "usage" : "Timeouts a user. <b>Usage: /timeout [user] [duration] [reason]</b>" },
|
||||
{ "name" : "/untimeout", "usage" : "Removes timeout from a user. <b>Usage: /untimeout [user]</b>" },
|
||||
{ "name" : "/ban", "usage" : "Bans a user. <b>Usage: /ban [user] [reason]</b>" },
|
||||
{ "name" : "/unban", "usage" : "Unbans a user. <b>Usage: /unban [user]</b>" },
|
||||
{ "name" : "/mod", "usage" : "Mod a user. <b>Usage: /mod [user]</b>" },
|
||||
{ "name" : "/unmod", "usage" : "Removes mod from a user. <b>Usage: /unmod [user]</b>" },
|
||||
{ "name" : "/vip", "usage" : "Adds user to VIP. <b>Usage: /vip [user]</b>" },
|
||||
{ "name" : "/unvip", "usage" : "Removes user from VIP. <b>Usage: /unvip [user]</b>" },
|
||||
{ "name" : "/shoutout", "usage" : "Shoutouts a user. <b>Usage: /shoutout [user]</b>" },
|
||||
{ "name" : "/raid", "usage" : "Raids a user. <b>Usage: /raid [user]</b>" },
|
||||
{ "name" : "/unraid", "usage" : "Removes the outcoming raid. <b>Usage: /unraid</b>" },
|
||||
{ "name" : "/settitle", "usage" : "Sets the stream title. <b>Usage: /settitle [title]</b>" },
|
||||
{ "name" : "/setgame", "usage" : "Sets the stream game. <b>Usage: /setgame [game]</b>" },
|
||||
],
|
||||
"YouTube" : [
|
||||
{ "name" : "/yt/title", "usage" : "Sets the stream title. <b>Usage: /yt/title [title]</b>" },
|
||||
{ "name" : "/yt/timeout", "usage" : "Times out a user. <b>Usage: /yt/timeout [user] [duration]</b>" },
|
||||
{ "name" : "/yt/ban", "usage" : "Bans a user. <b>Usage: /yt/ban [user]</b>" }
|
||||
],
|
||||
"Kick" : [
|
||||
{ "name" : "/kick/title", "usage" : "Sets the stream title. <b>Usage: /kick/title [title]</b>" },
|
||||
{ "name" : "/kick/category", "usage" : "Sets the stream category. <b>Usage: /kick/category [category]</b>" },
|
||||
{ "name" : "/kick/timeout", "usage" : "Times out a user. <b>Usage: /kick/timeout [user] [duration]</b>" },
|
||||
{ "name" : "/kick/untimeout", "usage" : "Removes timeout from a user. <b>Usage: /kick/untimeout [user]</b>" },
|
||||
{ "name" : "/kick/ban", "usage" : "Bans a user. <b>Usage: /kick/ban [user]</b>" },
|
||||
{ "name" : "/kick/unban", "usage" : "Unbans a user. <b>Usage: /kick/unban [user]</b>" }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
|
||||
function highlightItem(items) {
|
||||
if (!items) return;
|
||||
|
||||
items.forEach(item => item.classList.remove('active'));
|
||||
|
||||
if (chatcurrentFocus >= items.length) chatcurrentFocus = 0;
|
||||
if (chatcurrentFocus < 0) chatcurrentFocus = items.length - 1;
|
||||
|
||||
items[chatcurrentFocus].classList.add('active');
|
||||
items[chatcurrentFocus].scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
chatInput.addEventListener('input', function () {
|
||||
const value = this.value.trim();
|
||||
chatcommandslist.innerHTML = '';
|
||||
chatcurrentFocus = -1;
|
||||
if (!value.startsWith('/')) return;
|
||||
Object.entries(chatcommands).forEach(([groupName, commands]) => {
|
||||
|
||||
const filtered = commands.filter(cmd => cmd.name.startsWith(value));
|
||||
|
||||
if (filtered.length === 0) return;
|
||||
|
||||
const groupTitle = document.createElement('div');
|
||||
groupTitle.textContent = groupName;
|
||||
chatcommandslist.appendChild(groupTitle);
|
||||
filtered.forEach(cmd => {
|
||||
const item = document.createElement('div');
|
||||
item.classList.add('autocomplete-item');
|
||||
item.innerHTML = `<strong>${cmd.name}</strong><small> ${cmd.usage}</small>`;
|
||||
item.addEventListener('click', () => {
|
||||
chatInput.value = cmd.name + ' ';
|
||||
chatcommandslist.innerHTML = '';
|
||||
});
|
||||
chatcommandslist.appendChild(item);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
chatInput.addEventListener('keydown', function (e) {
|
||||
const items = chatcommandslist.querySelectorAll('.autocomplete-item');
|
||||
|
||||
if (items.length === 0) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
chatcurrentFocus++;
|
||||
highlightItem(items);
|
||||
}
|
||||
else if (e.key === 'ArrowUp') {
|
||||
chatcurrentFocus--;
|
||||
highlightItem(items);
|
||||
}
|
||||
|
||||
else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (chatcurrentFocus > -1 && items[chatcurrentFocus]) {
|
||||
items[chatcurrentFocus].click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
chatInputForm.addEventListener("submit", function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var chatSendPlatforms = [];
|
||||
|
||||
if (showTwitch == true && showTwitchMessages == true) { chatSendPlatforms.push('twitch'); }
|
||||
if (showYoutube == true && showYouTubeMessages == true) { chatSendPlatforms.push('youtube'); }
|
||||
if (showTiktok == true && showTikTokMessages == true) { chatSendPlatforms.push('tiktok'); }
|
||||
if (showKick == true && showKickMessages == true) { chatSendPlatforms.push('kick'); }
|
||||
|
||||
chatSendPlatforms = chatSendPlatforms.join(',')
|
||||
|
||||
const chatInput = chatInputForm.querySelector("input[type=text]")
|
||||
const chatInputText = chatInput.value;
|
||||
|
||||
// Sends Message to Twitch and YouTube
|
||||
streamerBotClient.doAction(
|
||||
{ name : "[Twitch][YouTube][Kick] Msgs/Cmds" },
|
||||
{
|
||||
"type": "chat",
|
||||
"platforms": chatSendPlatforms,
|
||||
"message": chatInputText,
|
||||
}
|
||||
).then( (sendchatstuff) => {
|
||||
console.debug('ChatRD] Sending Chat to Streamer.Bot', sendchatstuff);
|
||||
});
|
||||
|
||||
// Sends Message to TikTok that are not commands
|
||||
if (chatSendPlatforms.includes('tiktok')) {
|
||||
if (!chatInputText.startsWith('/')) {
|
||||
streamerBotClient.doAction(
|
||||
{ name : "[TikTok] Msgs" },
|
||||
{
|
||||
"ttkmessage": chatInputText,
|
||||
}
|
||||
).then( (sendchatstuff) => {
|
||||
console.debug('[ChatRD] Sending TikTok Chat to Streamer.Bot', sendchatstuff);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
chatInput.value = '';
|
||||
});
|
||||
|
||||
chatInputSend.addEventListener("click", function () {
|
||||
chatInputForm.requestSubmit();
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target !== chatcommandslist) {
|
||||
chatcommandslist.innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function speakerBotTTSRead(clone,type) {
|
||||
|
||||
var TTSMessage = "";
|
||||
|
||||
const {
|
||||
header,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
if (type == "chat") {
|
||||
var cleanmessage = "";
|
||||
|
||||
if (message == null) { cleanmessage = "<br>"; }
|
||||
else { cleanmessage = message.innerHTML; }
|
||||
|
||||
var strippedmessage = await cleanStringOfHTMLButEmotes(cleanmessage);
|
||||
|
||||
|
||||
const tts = {
|
||||
user: user.textContent,
|
||||
message: strippedmessage
|
||||
}
|
||||
|
||||
|
||||
TTSMessage = renderTemplate(speakerBotChatTemplate, tts);
|
||||
}
|
||||
|
||||
if (type == "event") {
|
||||
|
||||
var cleanvalue = "";
|
||||
if (value == null) { cleanvalue = ""; }
|
||||
else { cleanvalue = value.innerHTML; }
|
||||
|
||||
var cleanmessage = "";
|
||||
if (message == null) { cleanmessage = "<br>"; }
|
||||
else { cleanmessage = message.innerHTML; }
|
||||
|
||||
var strippedmessage = await cleanStringOfHTMLButEmotes(cleanmessage);
|
||||
var strippedaction = await cleanStringOfHTMLButEmotes(action.innerHTML);
|
||||
var strippedvalue = await cleanStringOfHTMLButEmotes(cleanvalue);
|
||||
|
||||
TTSMessage = user.textContent + strippedaction + strippedvalue + ". " + strippedmessage;
|
||||
}
|
||||
|
||||
speakerBotClient.speak(TTSMessage);
|
||||
|
||||
/*streamerBotClient.doAction({ name : "[Speakerbot] TTS" },
|
||||
{
|
||||
"message": TTSMessage,
|
||||
"alias" : speakerBotVoiceAlias
|
||||
}
|
||||
).then( (response) => {
|
||||
console.debug('[ChatRD][Streamer.bot -> Speaker.bot] Sending TTS...', response);
|
||||
});*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
function renderTemplate(template, data) {
|
||||
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
||||
return key in data ? data[key] : match;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function cleanStringOfHTMLButEmotes(string) {
|
||||
// Cria um elemento DOM temporário
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = string;
|
||||
|
||||
// Substitui <img class="emote" alt="..."> por texto do alt
|
||||
const emotes = container.querySelectorAll('img.emote[alt]');
|
||||
emotes.forEach(img => {
|
||||
const altText = img.getAttribute('alt');
|
||||
const textNode = document.createTextNode(altText);
|
||||
img.replaceWith(textNode);
|
||||
});
|
||||
|
||||
// Remove todo o restante do HTML
|
||||
return container.textContent || "";
|
||||
}
|
||||
|
||||
|
||||
async function executeModCommand(event, command) {
|
||||
event.preventDefault();
|
||||
chatInput.value = command;
|
||||
chatInputForm.requestSubmit();
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
12
js/modules/fourthwall/images/logo-fourthwall.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 800 800">
|
||||
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0" d="M330.1,230.7l-11.4,54.8-52.4,3.7-9.8,29.8h136.1l31.3,165.6,27.6-165.6,80.5.4,28,165.2,31.3-165.6h80.9l-55.2,250.2-91.8-.2-29.6-176.4-37.3,176.1-88-1.1-56.9-241.2c-8.2-3-5,7.8-6,12.3-2.6,11.7-4.9,23.5-7.2,35.2l-56.8,3.9-38.5,191.5h-77.3l40.5-193.1-33.2-1.9,9.1-51.7,34.6-4c6.5-28,12.3-60.5,39.7-76.2,5.2-3,31.8-11.6,36.2-11.6h75.4Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 677 B |
11
js/modules/fourthwall/module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
#chat .event.fourthwall .message {
|
||||
background: rgba(0,68,255,0.75);
|
||||
}
|
||||
|
||||
#chat .event.fourthwall .header {
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
}
|
||||
#chat .event.fourthwall .header img {
|
||||
height: 128px;
|
||||
}
|
291
js/modules/fourthwall/module.js
Normal file
@@ -0,0 +1,291 @@
|
||||
/* --------------------------- */
|
||||
/* FOURTHWALL MODULE VARIABLES */
|
||||
/* --------------------------- */
|
||||
|
||||
const showFourthwall = getURLParam("showFourthwall", true);
|
||||
|
||||
const showFourthwallDonations = getURLParam("showFourthwallDonations", true);
|
||||
const showFourthwallSubscriptions = getURLParam("showFourthwallSubscriptions", true);
|
||||
|
||||
const showFourthwallOrders = getURLParam("showFourthwallOrders", true);
|
||||
const showFourthwallShowImage = getURLParam("showFourthwallShowImage", true);
|
||||
|
||||
const showFourthwallGiftPurchase = getURLParam("showFourthwallGiftPurchase", true);
|
||||
const showFourthwallShowGiftImage = getURLParam("showFourthwallShowGiftImage", true);
|
||||
|
||||
const showFourthwallGiftDraw = getURLParam("showFourthwallGiftDraw", true);
|
||||
const fourthWallGiftDrawCommand = getURLParam("fourthWallGiftDrawCommand", "!enter");
|
||||
|
||||
|
||||
// FOURTHWALL EVENTS HANDLERS
|
||||
|
||||
const fourthwallMessageHandlers = {
|
||||
'Fourthwall.Donation': (response) => {
|
||||
fourthwallDonationMessage(response.data);
|
||||
},
|
||||
'Fourthwall.SubscriptionPurchased': (response) => {
|
||||
fourthwallSubMessage(response.data);
|
||||
},
|
||||
'Fourthwall.OrderPlaced': (response) => {
|
||||
fourthwallOrderMessage(response.data);
|
||||
},
|
||||
'Fourthwall.GiftPurchase': (response) => {
|
||||
fourthwallGiftMessage(response.data);
|
||||
},
|
||||
'Fourthwall.GiftDrawStarted': (response) => {
|
||||
fourthwallGiftDrawStartMessage(response.data);
|
||||
},
|
||||
'Fourthwall.GiftDrawEnded': (response) => {
|
||||
fourthwallGiftDrawEndMessage(response.data);
|
||||
},
|
||||
};
|
||||
|
||||
if (showFourthwall) {
|
||||
registerPlatformHandlersToStreamerBot(fourthwallMessageHandlers, '[Fourthwall]');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// FOURTHWALL EVENT FUNCTIONS
|
||||
|
||||
async function fourthwallDonationMessage(data) {
|
||||
|
||||
if (showFourthwallDonations == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['fourthwall', 'donation'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.username}</strong>`;
|
||||
action.innerHTML = ` donated `;
|
||||
|
||||
var money = formatCurrency(data.amount,data.currency);
|
||||
value.innerHTML = `<strong>${money}</strong>`;
|
||||
|
||||
if (data.message) message.innerHTML = `${data.message}`;
|
||||
|
||||
addEventItem('fourthwall', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function fourthwallOrderMessage(data) {
|
||||
|
||||
if (showFourthwallOrders == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const username = data.username;
|
||||
const total = data.total;
|
||||
const currency = data.currency;
|
||||
const item = data.variants[0].name;
|
||||
const itemsQuantity = data.variants.length;
|
||||
const text = stripStringFromHtml(data.statmessageus);
|
||||
const imageUrl = data?.variants?.[0]?.image ?? '';
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['fourthwall', 'order'];
|
||||
|
||||
if (showFourthwallShowImage == true) {
|
||||
if (imageUrl) { header.innerHTML = `<img src="${imageUrl}">`; }
|
||||
else { header.remove(); }
|
||||
}
|
||||
else { header.remove(); }
|
||||
|
||||
|
||||
var userName = '';
|
||||
if (username == undefined) { userName = 'Someone'; }
|
||||
else { userName = username; }
|
||||
|
||||
user.innerHTML = `<strong>${userName}</strong>`;
|
||||
action.innerHTML = ` ordered `;
|
||||
|
||||
var money = formatCurrency(total,currency);
|
||||
var html = `<strong>${item}</strong>`;
|
||||
|
||||
if (itemsQuantity > 1) { html += ` and <strong>${itemsQuantity - 1} other ${(itemsQuantity - 1) == 1 ? 'item' : 'items'}</strong> (${total == 0 ? 'Free' : money})`; }
|
||||
else { html += ` (${total == 0 ? 'Free' : money})`; }
|
||||
|
||||
value.innerHTML = html;
|
||||
|
||||
if (text) message.innerHTML = `${text}`;
|
||||
|
||||
addEventItem('fourthwall', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function fourthwallGiftMessage(data) {
|
||||
|
||||
if (showFourthwallGiftPurchase == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const username = data.username;
|
||||
const total = data.total;
|
||||
const currency = data.currency;
|
||||
const item = data.offer.name;
|
||||
const itemsQuantity = data.gifts.length;
|
||||
const text = stripStringFromHtml(data.statmessageus);
|
||||
const imageUrl = data?.offer?.imageUrl ?? '';
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['fourthwall', 'gift'];
|
||||
|
||||
if (showFourthwallShowGiftImage == true) {
|
||||
if (imageUrl) { header.innerHTML = `<img src="${imageUrl}">`; }
|
||||
else { header.remove(); }
|
||||
}
|
||||
else { header.remove(); }
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${userName}</strong>`;
|
||||
action.innerHTML = ` gifted `;
|
||||
|
||||
var money = formatCurrency(total,currency);
|
||||
var html = `<strong>${itemsQuantity} ${item}</strong>`;
|
||||
html += ` (${total == 0 ? 'Free' : money})`;
|
||||
|
||||
value.innerHTML = html;
|
||||
|
||||
if (text) message.innerHTML = `${text}`;
|
||||
|
||||
addEventItem('fourthwall', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function fourthwallGiftDrawStartMessage(data) {
|
||||
|
||||
if (showFourthwallGiftDraw == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['fourthwall', 'giftdraw'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong><i class="fa-solid fa-gifts"></i> Giveaway started!</strong>`;
|
||||
action.innerHTML = ` Type <strong>${fourthWallGiftDrawCommand}</strong> to have a chance to win `;
|
||||
value.innerHTML = `<strong>${data.offer.name}</strong>. `
|
||||
message.innerHTML = `You have <strong>${durationSeconds}</strong> seconds! Good Luck!`;
|
||||
|
||||
addEventItem('fourthwall', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function fourthwallGiftDrawEndMessage(data) {
|
||||
|
||||
if (showFourthwallGiftDraw == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['fourthwall', 'giftdrawend'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>🎉 Giveaway Ended!</strong>`;
|
||||
action.innerHTML = ` Congratulations to: `;
|
||||
value.remove();
|
||||
|
||||
var winners = await getWinnersList(data.gifts);
|
||||
|
||||
message.innerHTML = `${winners}`;
|
||||
|
||||
addEventItem('fourthwall', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
async function getWinnersList(gifts) {
|
||||
const winners = gifts.map(gift => gift.winner).filter(Boolean); // Remove null/undefined
|
||||
|
||||
const numWinners = winners.length;
|
||||
|
||||
if (numWinners === 0) { return ""; }
|
||||
if (numWinners === 1) { return winners[0]; }
|
||||
if (numWinners === 2) { return `${winners[0]} and ${winners[1]}`; }
|
||||
|
||||
// For 3 or more, use the Oxford comma style: A, B, and C
|
||||
const allButLast = winners.slice(0, -1).join(", ");
|
||||
const lastWinner = winners[winners.length - 1];
|
||||
return `${allButLast}, and ${lastWinner}`;
|
||||
}
|
1
js/modules/kick/images/badge-bot.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="size-[calc(1em*(18/13))]"><g clip-path="url(#clip0_209_29907)"><path d="M17.56 0H14.4533C13.717 0 13.12 0.597452 13.12 1.33445V4.4437C13.12 5.1807 13.717 5.77815 14.4533 5.77815H17.56C18.2964 5.77815 18.8933 5.1807 18.8933 4.4437V1.33445C18.8933 0.597452 18.2964 0 17.56 0Z" fill="url(#paint0_linear_209_29907)"></path><path d="M17.3333 5.77815H14.6667V8.44704H17.3333V5.77815Z" fill="url(#paint1_linear_209_29907)"></path><path d="M5.33333 14.8257C5.33333 14.8257 0 14.8257 0 20.1635C0 25.5013 5.33333 25.5013 5.33333 25.5013V14.8257Z" fill="url(#paint2_linear_209_29907)"></path><path d="M26.6667 14.8257C26.6667 14.8257 32 14.8257 32 20.1635C32 25.5013 26.6667 25.5013 26.6667 25.5013V14.8257Z" fill="url(#paint3_linear_209_29907)"></path><path d="M26.6667 10.8224H5.33333V28.1701H26.6667V10.8224Z" fill="#4FD8FF"></path><path d="M15.76 11.2761C20.4133 11.2761 24.0933 12.3036 25.24 13.2911C26.3867 14.8657 26.1733 24.4737 24.9067 26.0751C24.2533 26.849 21.0667 28.01 16.28 28.01C11.24 28.01 7.73333 26.7556 6.94667 25.9283C5.70667 24.367 5.65333 14.7189 6.84 13.211C7.58667 12.4637 10.6667 11.2761 15.76 11.2761ZM15.76 7.27273C10.9067 7.27273 6.12 8.28691 4.01333 10.382C1.25333 13.1309 1.34667 25.7948 4.01333 28.6372C6.08 30.8524 11.2133 32 16.28 32C21.3467 32 26.08 30.9058 27.9867 28.6372C30.48 25.648 30.8667 12.9975 27.9867 10.382C25.7467 8.34028 20.72 7.27273 15.76 7.27273Z" fill="url(#paint4_linear_209_29907)"></path><path d="M23 17.975C23 16.6852 21.9553 15.6397 20.6667 15.6397C19.378 15.6397 18.3333 16.6852 18.3333 17.975V21.3111C18.3333 22.6008 19.378 23.6464 20.6667 23.6464C21.9553 23.6464 23 22.6008 23 21.3111V17.975Z" fill="black"></path><path d="M13.6667 17.975C13.6667 16.6852 12.622 15.6397 11.3333 15.6397C10.0447 15.6397 9 16.6852 9 17.975V21.3111C9 22.6008 10.0447 23.6464 11.3333 23.6464C12.622 23.6464 13.6667 22.6008 13.6667 21.3111V17.975Z" fill="black"></path></g><defs><linearGradient id="paint0_linear_209_29907" x1="15.963" y1="0.886836" x2="15.963" y2="43.3072" gradientUnits="userSpaceOnUse"><stop stop-color="#00C7FF"></stop><stop offset="0.99" stop-color="#006399"></stop></linearGradient><linearGradient id="paint1_linear_209_29907" x1="16" y1="-69.28" x2="16" y2="-69.28" gradientUnits="userSpaceOnUse"><stop stop-color="#00C7FF"></stop><stop offset="0.99" stop-color="#006399"></stop></linearGradient><linearGradient id="paint2_linear_209_29907" x1="17.28" y1="-0.2" x2="16.8598" y2="31.7766" gradientUnits="userSpaceOnUse"><stop stop-color="#00C7FF"></stop><stop offset="0.99" stop-color="#006399"></stop></linearGradient><linearGradient id="paint3_linear_209_29907" x1="14.72" y1="-0.2" x2="15.1402" y2="31.7766" gradientUnits="userSpaceOnUse"><stop stop-color="#00C7FF"></stop><stop offset="0.99" stop-color="#006399"></stop></linearGradient><linearGradient id="paint4_linear_209_29907" x1="5.14015" y1="0.587156" x2="36.6592" y2="34.8544" gradientUnits="userSpaceOnUse"><stop stop-color="#00C7FF"></stop><stop offset="0.99" stop-color="#006399"></stop></linearGradient><clipPath id="clip0_209_29907"><rect width="32" height="32" fill="white"></rect></clipPath></defs></svg>
|
After Width: | Height: | Size: 3.1 KiB |
1
js/modules/kick/images/badge-broadcaster.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="size-[calc(1em*(18/13))]"><path d="M15.6773 22.1533C17.3698 22.1533 18.8182 21.5507 20.0233 20.3461C21.2282 19.1415 21.8307 17.6924 21.8307 16V6.15401C21.8307 4.46162 21.2286 3.01305 20.0233 1.80784C18.8182 0.602907 17.3698 0 15.6773 0C13.9849 0 12.5363 0.602907 11.3311 1.80784C10.1259 3.01285 9.52344 4.46162 9.52344 6.15401V16C9.52344 17.6923 10.1262 19.1415 11.3311 20.3461C12.5361 21.5507 13.9849 22.1533 15.6773 22.1533Z" fill="url(#paint0_linear_209_29909)"></path><path d="M15.6773 22.1533C17.3698 22.1533 18.8182 21.5507 20.0233 20.3461C21.2282 19.1415 21.8307 17.6924 21.8307 16V6.15401C21.8307 4.46162 21.2286 3.01305 20.0233 1.80784C18.8182 0.602907 17.3698 0 15.6773 0C13.9849 0 12.5363 0.602907 11.3311 1.80784C10.1259 3.01285 9.52344 4.46162 9.52344 6.15401V16C9.52344 17.6923 10.1262 19.1415 11.3311 20.3461C12.5361 21.5507 13.9849 22.1533 15.6773 22.1533Z" fill="white" fill-opacity="0.3"></path><path d="M26.3888 12.6731C26.1459 12.4295 25.8568 12.3076 25.5234 12.3076C25.1904 12.3076 24.902 12.4295 24.6581 12.6731C24.4147 12.9167 24.293 13.2051 24.293 13.5383V16C24.293 18.3718 23.4498 20.4006 21.7639 22.0864C20.0785 23.7723 18.0495 24.6153 15.6775 24.6153C13.3057 24.6153 11.2769 23.7723 9.59089 22.0864C7.90509 20.401 7.06226 18.3719 7.06226 16V13.5383C7.06226 13.2051 6.94041 12.9167 6.69692 12.6731C6.45329 12.4295 6.16514 12.3076 5.83159 12.3076C5.49804 12.3076 5.20956 12.4295 4.96606 12.6731C4.72237 12.9167 4.60059 13.2051 4.60059 13.5383V16C4.60059 18.8333 5.54627 21.2981 7.4371 23.3941C9.32799 25.4901 11.6645 26.6919 14.4467 26.9994V29.5381H9.52373C9.19038 29.5381 8.90196 29.6601 8.6584 29.9037C8.41477 30.1472 8.29293 30.4357 8.29293 30.7691C8.29293 31.1019 8.41477 31.391 8.6584 31.6344C8.90196 31.8778 9.19038 32 9.52373 32H21.831C22.1643 32 22.4531 31.8779 22.6963 31.6344C22.9402 31.391 23.0622 31.1019 23.0622 30.7691C23.0622 30.4358 22.9402 30.1472 22.6963 29.9037C22.4532 29.6601 22.1644 29.5381 21.831 29.5381H16.9086V26.9994C19.6904 26.6919 22.0267 25.4901 23.9178 23.3941C25.8089 21.2981 26.7548 18.8333 26.7548 16V13.5383C26.7548 13.2051 26.6327 12.9169 26.3888 12.6731Z" fill="url(#paint1_linear_209_29909)"></path><defs><linearGradient id="paint0_linear_209_29909" x1="5.22969e-08" y1="-5.22969e-08" x2="32" y2="32" gradientUnits="userSpaceOnUse"><stop stop-color="#FF1CD2"></stop><stop offset="1" stop-color="#B20DFF"></stop></linearGradient><linearGradient id="paint1_linear_209_29909" x1="-5.88081e-07" y1="-8.98822e-07" x2="4.72839" y2="35.6202" gradientUnits="userSpaceOnUse"><stop stop-color="#FF1CD2"></stop><stop offset="1" stop-color="#B20DFF"></stop></linearGradient></defs></svg>
|
After Width: | Height: | Size: 2.7 KiB |
1
js/modules/kick/images/badge-founder.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="size-[calc(1em*(18/13))]"><g clip-path="url(#clip0_209_29912)"><path d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32Z" fill="url(#paint0_linear_209_29912)"></path><path d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32Z" fill="url(#paint1_linear_209_29912)"></path><path d="M16 29.0375C23.2004 29.0375 29.0375 23.2004 29.0375 16C29.0375 8.79958 23.2004 2.96249 16 2.96249C8.79959 2.96249 2.9625 8.79958 2.9625 16C2.9625 23.2004 8.79959 29.0375 16 29.0375Z" fill="#FEB635"></path><path d="M16 29.0375C23.2004 29.0375 29.0375 23.2004 29.0375 16C29.0375 8.79958 23.2004 2.96249 16 2.96249C8.79959 2.96249 2.9625 8.79958 2.9625 16C2.9625 23.2004 8.79959 29.0375 16 29.0375Z" fill="url(#paint2_linear_209_29912)"></path><path d="M29.0375 16C29.0375 23.1875 23.1875 29.0375 16 29.0375C13.6563 29.0375 11.4625 28.4187 9.5625 27.3312C11.3125 28.2062 13.2875 28.7 15.375 28.7C22.5625 28.7 28.4125 22.85 28.4125 15.6625C28.4125 10.8188 25.75 6.58125 21.8125 4.3375C26.0938 6.475 29.0375 10.8938 29.0375 16ZM16.8875 3.575C19.4563 3.575 21.85 4.325 23.8625 5.60625C21.675 3.95625 18.95 2.96875 16 2.96875C8.8125 2.96875 2.9625 8.8125 2.9625 16.0063C2.9625 20.6437 5.4 24.7313 9.0625 27.0312C5.9 24.65 3.85 20.8687 3.85 16.6125C3.85 9.425 9.7 3.575 16.8875 3.575Z" fill="black" fill-opacity="0.05"></path><path d="M18.5966 9.45456V24H14.6477V13.0909H14.5625L11.3807 14.9943V11.6421L14.9602 9.45456H18.5966Z" fill="black" fill-opacity="0.8"></path><path d="M18.5966 9.45456V24H14.6477V13.0909H14.5625L11.3807 14.9943V11.6421L14.9602 9.45456H18.5966Z" fill="url(#paint3_linear_209_29912)" fill-opacity="0.5"></path><path d="M18.5966 9.45456V24H14.6477V13.0909H14.5625L11.3807 14.9943V11.6421L14.9602 9.45456H18.5966Z" stroke="black" stroke-opacity="0.1" stroke-width="0.350269"></path></g><defs><linearGradient id="paint0_linear_209_29912" x1="15.7467" y1="-4.46667" x2="16.2533" y2="36.6933" gradientUnits="userSpaceOnUse"><stop stop-color="#FFC900"></stop><stop offset="0.99" stop-color="#FF9500"></stop></linearGradient><linearGradient id="paint1_linear_209_29912" x1="16" y1="0" x2="16" y2="32" gradientUnits="userSpaceOnUse"><stop stop-color="white" stop-opacity="0.3"></stop><stop offset="1" stop-color="white" stop-opacity="0.15"></stop></linearGradient><linearGradient id="paint2_linear_209_29912" x1="15.7936" y1="-0.677142" x2="16.2064" y2="32.8618" gradientUnits="userSpaceOnUse"><stop stop-color="#FFC900"></stop><stop offset="0.99" stop-color="#FF9500"></stop></linearGradient><linearGradient id="paint3_linear_209_29912" x1="18.5966" y1="16.7273" x2="11.3807" y2="16.7273" gradientUnits="userSpaceOnUse"><stop stop-color="white" stop-opacity="0.1"></stop><stop offset="0.3" stop-color="white" stop-opacity="0.2"></stop><stop offset="0.65" stop-color="white" stop-opacity="0.05"></stop><stop offset="1" stop-color="white" stop-opacity="0.2"></stop></linearGradient><clipPath id="clip0_209_29912"><rect width="32" height="32" fill="white"></rect></clipPath></defs></svg>
|
After Width: | Height: | Size: 3.1 KiB |
1
js/modules/kick/images/badge-moderator.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="size-[calc(1em*(18/13))]"><g clip-path="url(#clip0_817_50667)"><path d="M30 0C31.1046 0 32 0.895431 32 2V30C32 31.1046 31.1046 32 30 32H2C0.895431 32 0 31.1046 0 30V2C0 0.895431 0.895431 0 2 0H30ZM16.2197 2.99316C15.8292 2.60266 15.1962 2.60265 14.8057 2.99316L8.36328 9.43555C7.97294 9.82608 7.97284 10.4591 8.36328 10.8496L10.0918 12.5781C10.4823 12.9686 11.1153 12.9685 11.5059 12.5781L11.585 12.499L13.9414 14.8564L3.57129 25.2275C2.70357 26.0954 2.7035 27.5023 3.57129 28.3701C4.43911 29.2376 5.84612 29.2377 6.71387 28.3701L17.084 17.999L19.4414 20.3564L19.3633 20.4346C18.9728 20.8251 18.9728 21.4581 19.3633 21.8486L21.0918 23.5771C21.4823 23.9676 22.1154 23.9676 22.5059 23.5771L28.9482 17.1348C29.3386 16.7443 29.3386 16.1112 28.9482 15.7207L27.2197 13.9922C26.8293 13.6017 26.1962 13.6018 25.8057 13.9922L25.7266 14.0703L23.3701 11.7139C24.2377 10.8461 24.2376 9.4391 23.3701 8.57129C22.5023 7.7035 21.0954 7.70357 20.2275 8.57129L17.8701 6.21387L17.9482 6.13574C18.3388 5.74522 18.3388 5.11221 17.9482 4.72168L16.2197 2.99316Z" fill="url(#paint0_linear_817_50667)"></path><path d="M30 0C31.1046 0 32 0.895431 32 2V30C32 31.1046 31.1046 32 30 32H2C0.895431 32 0 31.1046 0 30V2C0 0.895431 0.895431 0 2 0H30ZM16.2197 2.99316C15.8292 2.60266 15.1962 2.60265 14.8057 2.99316L8.36328 9.43555C7.97294 9.82608 7.97284 10.4591 8.36328 10.8496L10.0918 12.5781C10.4823 12.9686 11.1153 12.9685 11.5059 12.5781L11.585 12.499L13.9414 14.8564L3.57129 25.2275C2.70357 26.0954 2.7035 27.5023 3.57129 28.3701C4.43911 29.2376 5.84612 29.2377 6.71387 28.3701L17.084 17.999L19.4414 20.3564L19.3633 20.4346C18.9728 20.8251 18.9728 21.4581 19.3633 21.8486L21.0918 23.5771C21.4823 23.9676 22.1154 23.9676 22.5059 23.5771L28.9482 17.1348C29.3386 16.7443 29.3386 16.1112 28.9482 15.7207L27.2197 13.9922C26.8293 13.6017 26.1962 13.6018 25.8057 13.9922L25.7266 14.0703L23.3701 11.7139C24.2377 10.8461 24.2376 9.4391 23.3701 8.57129C22.5023 7.7035 21.0954 7.70357 20.2275 8.57129L17.8701 6.21387L17.9482 6.13574C18.3388 5.74522 18.3388 5.11221 17.9482 4.72168L16.2197 2.99316Z" fill="url(#paint1_linear_817_50667)"></path><path d="M30 0C31.1046 0 32 0.895431 32 2V30C32 31.1046 31.1046 32 30 32H2C0.895431 32 0 31.1046 0 30V2C0 0.895431 0.895431 0 2 0H30ZM16.2197 2.99316C15.8292 2.60266 15.1962 2.60265 14.8057 2.99316L8.36328 9.43555C7.97294 9.82608 7.97284 10.4591 8.36328 10.8496L10.0918 12.5781C10.4823 12.9686 11.1153 12.9685 11.5059 12.5781L11.585 12.499L13.9414 14.8564L3.57129 25.2275C2.70357 26.0954 2.7035 27.5023 3.57129 28.3701C4.43911 29.2376 5.84612 29.2377 6.71387 28.3701L17.084 17.999L19.4414 20.3564L19.3633 20.4346C18.9728 20.8251 18.9728 21.4581 19.3633 21.8486L21.0918 23.5771C21.4823 23.9676 22.1154 23.9676 22.5059 23.5771L28.9482 17.1348C29.3386 16.7443 29.3386 16.1112 28.9482 15.7207L27.2197 13.9922C26.8293 13.6017 26.1962 13.6018 25.8057 13.9922L25.7266 14.0703L23.3701 11.7139C24.2377 10.8461 24.2376 9.4391 23.3701 8.57129C22.5023 7.7035 21.0954 7.70357 20.2275 8.57129L17.8701 6.21387L17.9482 6.13574C18.3388 5.74522 18.3388 5.11221 17.9482 4.72168L16.2197 2.99316Z" fill="url(#paint2_linear_817_50667)"></path></g><defs><linearGradient id="paint0_linear_817_50667" x1="18.8102" y1="-12.7222" x2="2.88536" y2="39.1063" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6A4A"></stop><stop offset="1" stop-color="#C70C00"></stop></linearGradient><linearGradient id="paint1_linear_817_50667" x1="15.7467" y1="-4.75575" x2="16.321" y2="39.0672" gradientUnits="userSpaceOnUse"><stop stop-color="#FFC900"></stop><stop offset="0.99" stop-color="#FF9500"></stop></linearGradient><linearGradient id="paint2_linear_817_50667" x1="-14.9543" y1="46.9544" x2="32.0001" y2="-0.000509222" gradientUnits="userSpaceOnUse"><stop stop-color="#0095FF"></stop><stop offset="0.99" stop-color="#00C7FF"></stop></linearGradient><clipPath id="clip0_817_50667"><rect width="32" height="32" fill="white"></rect></clipPath></defs></svg>
|
After Width: | Height: | Size: 3.9 KiB |
1
js/modules/kick/images/badge-og.svg
Normal file
After Width: | Height: | Size: 6.7 KiB |
1
js/modules/kick/images/badge-sidekick.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 32 32" class="size-[calc(1em*(18/13))]"><path fill="url(#SidekickBadgeA)" d="M0 5.5v11.3h2.3V20h2.3v3.2h2.2v3.2h6.9v-3.2h4.6v3.2H25v-3.2h2.3V20h2.3v-3.2H32V5.5h-9.2v3.2h-4.6V12h-4.5V8.7H9V5.5H0Zm13.7 13.7H7V16H4.6V9.6h2.3v3.2h4.5V16h2.3v3.2ZM27.4 16h-2.2v3.2h-6.9V16h2.3v-3.2h4.6V9.6h2.2V16Z"></path><defs><linearGradient id="SidekickBadgeA" x1="18.8" x2="11.7" y1="-2.7" y2="32.7" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6A4A"></stop><stop offset="1" stop-color="#C70C00"></stop></linearGradient></defs></svg>
|
After Width: | Height: | Size: 587 B |
1
js/modules/kick/images/badge-sub_gifter.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="size-[calc(1em*(18/13))]"><g clip-path="url(#clip0_31_3197)"><path d="M22.34 9.5L26 4H18L16 7L14 4H6L9.66 9.5H4V15.1H28V9.5H22.34Z" fill="#2EFAD1"></path><path d="M26.08 19.1001H5.90002V28.5001H26.08V19.1001Z" fill="#2EFAD1"></path><path d="M26.08 15.1001H5.90002V19.1001H26.08V15.1001Z" fill="#00A18D"></path></g><defs><clipPath id="clip0_31_3197"><rect width="24" height="24.5" fill="white" transform="translate(4 4)"></rect></clipPath></defs></svg>
|
After Width: | Height: | Size: 530 B |
1
js/modules/kick/images/badge-subscriber.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="size-[calc(1em*(18/13))]"><g clip-path="url(#clip0_209_29946)"><path d="M17.0284 2.91378L16.2357 0.667951C16.1573 0.445558 15.8427 0.445558 15.7643 0.667951L14.9716 2.91378C12.9003 8.78263 8.78263 12.9003 2.91378 14.9716L0.667951 15.7643C0.445558 15.8427 0.445558 16.1573 0.667951 16.2357L2.91378 17.0284C8.78263 19.0998 12.9003 23.2174 14.9716 29.0862L15.7643 31.3321C15.8427 31.5544 16.1573 31.5544 16.2357 31.3321L17.0284 29.0862C19.0998 23.2174 23.2174 19.0998 29.0862 17.0284L31.3321 16.2357C31.5544 16.1573 31.5544 15.8427 31.3321 15.7643L29.0862 14.9716C23.2174 12.9003 19.0998 8.78263 17.0284 2.91378Z" fill="black"></path><path d="M17.0284 2.91378L16.2357 0.667951C16.1573 0.445558 15.8427 0.445558 15.7643 0.667951L14.9716 2.91378C12.9003 8.78263 8.78263 12.9003 2.91378 14.9716L0.667951 15.7643C0.445558 15.8427 0.445558 16.1573 0.667951 16.2357L2.91378 17.0284C8.78263 19.0998 12.9003 23.2174 14.9716 29.0862L15.7643 31.3321C15.8427 31.5544 16.1573 31.5544 16.2357 31.3321L17.0284 29.0862C19.0998 23.2174 23.2174 19.0998 29.0862 17.0284L31.3321 16.2357C31.5544 16.1573 31.5544 15.8427 31.3321 15.7643L29.0862 14.9716C23.2174 12.9003 19.0998 8.78263 17.0284 2.91378Z" fill="url(#paint0_radial_209_29946)"></path></g><defs><radialGradient id="paint0_radial_209_29946" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(16 16) rotate(90) scale(16)"><stop stop-color="#E1FF00"></stop><stop offset="1" stop-color="#2AA300"></stop></radialGradient><clipPath id="clip0_209_29946"><rect width="32" height="32" fill="white"></rect></clipPath></defs></svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
js/modules/kick/images/badge-verified.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="shrink-0"><g clip-path="url(#clip0_614_6275)"><path d="M30.8598 19.2368C30.1977 18.2069 29.5356 17.2138 28.8736 16.1839C28.7264 15.9632 28.7264 15.8161 28.8736 15.5954C29.5356 14.6023 30.1609 13.6092 30.823 12.6161C31.5954 11.4391 31.1908 10.2989 29.8667 9.82069C28.7632 9.41609 27.6598 8.97471 26.5563 8.57012C26.3356 8.49656 26.2253 8.34943 26.2253 8.09196C26.1885 6.87816 26.1149 5.66437 26.0414 4.48736C25.9678 3.2 24.9747 2.46437 23.7241 2.7954C22.5471 3.08966 21.3701 3.42069 20.2299 3.75173C19.9724 3.82529 19.8253 3.75173 19.6414 3.56782C18.9057 2.61149 18.1333 1.69195 17.3977 0.772414C16.5885 -0.257472 15.3379 -0.257472 14.492 0.772414C13.7563 1.69195 12.9839 2.61149 12.2851 3.53103C12.1012 3.7885 11.9172 3.82529 11.623 3.75173C10.4828 3.42069 9.34253 3.12644 8.53334 2.90575C6.95173 2.53793 5.99541 3.16322 5.92184 4.48736C5.84828 5.70115 5.77472 6.91495 5.73794 8.16552C5.73794 8.42299 5.62759 8.53333 5.4069 8.64368C4.26667 9.08506 3.12644 9.52644 1.98621 9.96782C0.809203 10.446 0.441387 11.5862 1.14023 12.6529C1.8023 13.6828 2.46437 14.6759 3.12644 15.7057C3.27356 15.9264 3.27356 16.0736 3.12644 16.331C2.42759 17.3609 1.76552 18.3908 1.10345 19.4575C0.478165 20.4506 0.882759 21.6276 1.98621 22.069C3.12644 22.5104 4.30345 22.9517 5.44368 23.3931C5.70115 23.4667 5.77471 23.6138 5.77471 23.8713C5.81149 25.0483 5.95862 26.1885 5.95862 27.3655C5.95862 28.5425 6.9885 29.6092 8.42298 29.1678C9.56321 28.8 10.7034 28.5425 11.8437 28.2115C12.0644 28.1379 12.2115 28.1747 12.3586 28.3954C13.131 29.3517 13.8667 30.2713 14.6391 31.2276C15.485 32.2575 16.6988 32.2575 17.508 31.2276C18.2805 30.2713 19.0161 29.3517 19.7885 28.3954C19.9356 28.2115 20.046 28.1379 20.3034 28.2115C21.4804 28.5425 22.6575 28.8368 23.8345 29.1678C25.0483 29.4988 26.0781 28.7632 26.1149 27.5126C26.1885 26.2989 26.2621 25.0851 26.2988 23.8345C26.2988 23.5402 26.446 23.4299 26.6667 23.3563C27.7701 22.9517 28.9103 22.5104 30.0138 22.069C31.1908 21.4805 31.5586 20.3034 30.8598 19.2368ZM22.069 13.2046L14.7127 20.5609C14.5287 20.7448 14.2713 20.892 14.0138 20.9287C13.9402 20.9287 13.8299 20.9655 13.7563 20.9655C13.4253 20.9655 13.0575 20.8184 12.8 20.5609L9.78392 17.5448C9.26898 17.0299 9.26898 16.1839 9.78392 15.669C10.2989 15.154 11.1448 15.154 11.6598 15.669L13.7196 17.7287L20.1196 11.3287C20.6345 10.8138 21.4805 10.8138 21.9954 11.3287C22.5839 11.8437 22.5839 12.6897 22.069 13.2046Z" fill="url(#paint0_linear_614_6275)"></path></g><defs><linearGradient id="paint0_linear_614_6275" x1="8.14138" y1="32.3591" x2="24.4968" y2="0.904884" gradientUnits="userSpaceOnUse"><stop stop-color="#1EFF00"></stop><stop offset="0.99" stop-color="#00FF8C"></stop></linearGradient><clipPath id="clip0_614_6275"><rect width="32" height="32" fill="white"></rect></clipPath></defs></svg>
|
After Width: | Height: | Size: 2.8 KiB |
1
js/modules/kick/images/badge-vip.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="size-[calc(1em*(18/13))]"><g clip-path="url(#clip0_746_28171)"><path d="M30 0C31.1046 0 32 0.895431 32 2V30C32 31.1046 31.1046 32 30 32H2C0.895431 32 0 31.1046 0 30V2C0 0.895431 0.895431 4.10637e-08 2 0H30ZM15.9648 5C15.7748 5.00005 15.588 5.05204 15.4238 5.15039C15.2596 5.24878 15.124 5.39057 15.0303 5.56055L9.82812 15.0176L3.55078 11.8906C3.36913 11.7985 3.16534 11.7607 2.96387 11.7822C2.76241 11.8038 2.57048 11.8842 2.41113 12.0127C2.25235 12.1408 2.13185 12.3126 2.06348 12.5078C1.99511 12.7031 1.98143 12.9144 2.02441 13.1172L4.58301 25.127C4.63544 25.3782 4.77165 25.6034 4.96777 25.7627C5.16376 25.9217 5.40762 26.0056 5.65723 26H26.251C26.5009 26.0057 26.7453 25.9219 26.9414 25.7627C27.1376 25.6034 27.2737 25.3782 27.3262 25.127L29.9697 13.1172C30.0187 12.9103 30.0086 12.6932 29.9404 12.4922C29.8722 12.2912 29.7485 12.1151 29.585 11.9844C29.4215 11.8537 29.2249 11.7743 29.0186 11.7559C28.8122 11.7374 28.6049 11.7802 28.4219 11.8799L22.1025 15.0283L16.9004 5.56055C16.8066 5.39054 16.6701 5.24878 16.5059 5.15039C16.3416 5.05207 16.1549 5 15.9648 5Z" fill="url(#paint0_linear_746_28171)"></path><path d="M30 0C31.1046 0 32 0.895431 32 2V30C32 31.1046 31.1046 32 30 32H2C0.895431 32 0 31.1046 0 30V2C0 0.895431 0.895431 4.10637e-08 2 0H30ZM15.9648 5C15.7748 5.00005 15.588 5.05204 15.4238 5.15039C15.2596 5.24878 15.124 5.39057 15.0303 5.56055L9.82812 15.0176L3.55078 11.8906C3.36913 11.7985 3.16534 11.7607 2.96387 11.7822C2.76241 11.8038 2.57048 11.8842 2.41113 12.0127C2.25235 12.1408 2.13185 12.3126 2.06348 12.5078C1.99511 12.7031 1.98143 12.9144 2.02441 13.1172L4.58301 25.127C4.63544 25.3782 4.77165 25.6034 4.96777 25.7627C5.16376 25.9217 5.40762 26.0056 5.65723 26H26.251C26.5009 26.0057 26.7453 25.9219 26.9414 25.7627C27.1376 25.6034 27.2737 25.3782 27.3262 25.127L29.9697 13.1172C30.0187 12.9103 30.0086 12.6932 29.9404 12.4922C29.8722 12.2912 29.7485 12.1151 29.585 11.9844C29.4215 11.8537 29.2249 11.7743 29.0186 11.7559C28.8122 11.7374 28.6049 11.7802 28.4219 11.8799L22.1025 15.0283L16.9004 5.56055C16.8066 5.39054 16.6701 5.24878 16.5059 5.15039C16.3416 5.05207 16.1549 5 15.9648 5Z" fill="url(#paint1_linear_746_28171)"></path></g><defs><linearGradient id="paint0_linear_746_28171" x1="18.8102" y1="-12.7222" x2="2.88536" y2="39.1063" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6A4A"></stop><stop offset="1" stop-color="#C70C00"></stop></linearGradient><linearGradient id="paint1_linear_746_28171" x1="15.7467" y1="-4.75575" x2="16.321" y2="39.0672" gradientUnits="userSpaceOnUse"><stop stop-color="#FFC900"></stop><stop offset="0.99" stop-color="#FF9500"></stop></linearGradient><clipPath id="clip0_746_28171"><rect width="32" height="32" fill="white"></rect></clipPath></defs></svg>
|
After Width: | Height: | Size: 2.7 KiB |
12
js/modules/kick/images/logo-kick.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 800 800">
|
||||
<!-- Generator: Adobe Illustrator 29.6.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 9) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #53fc18;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<polygon class="st0" points="336 111 336 239 401 239 401 175 465 175 465 111 657 111 657 303 593 303 593 367 529 367 529 432 593 432 593 496 657 496 657 689 465 689 465 624 401 624 401 560 336 560 336 689 143 689 143 111 336 111"/>
|
||||
</svg>
|
After Width: | Height: | Size: 555 B |
24
js/modules/kick/module.css
Normal file
@@ -0,0 +1,24 @@
|
||||
#chat .event.kick .message {
|
||||
background: rgba(72,212,21,0.75);
|
||||
}
|
||||
|
||||
#chat .item.kick .platform .hidden-platform {
|
||||
background: rgba(72,212,21,1);
|
||||
}
|
||||
|
||||
#chat .item.kick .badges img {
|
||||
margin: 0 -1px;
|
||||
}
|
||||
|
||||
#chat .item .info .avatar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#chat .item .info .avatar img {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#statistics .platform#kick { background: #48d415; }
|
726
js/modules/kick/module.js
Normal file
@@ -0,0 +1,726 @@
|
||||
/* ---------------------- */
|
||||
/* KICK MODULE VARIABLES */
|
||||
/* ---------------------- */
|
||||
|
||||
const showKick = getURLParam("showKick", true);
|
||||
|
||||
const kickUserName = getURLParam("kickUserName", "vortisrd");
|
||||
|
||||
const showKickMessages = getURLParam("showKickMessages", true);
|
||||
const showKickFollows = getURLParam("showKickFollows", true);
|
||||
const showKickSubs = getURLParam("showKickSubs", true);
|
||||
const showKickGiftedSubs = getURLParam("showKickGiftedSubs", true);
|
||||
const showKickMassGiftedSubs = getURLParam("showKickMassGiftedSubs", true);
|
||||
const showKickGiftedSubsUserTrain = getURLParam("showKickGiftedSubsUserTrain", true);
|
||||
const showKickRewardRedemptions = getURLParam("showKickRewardRedemptions", true);
|
||||
const showKickRaids = getURLParam("showKickRaids", true);
|
||||
const showKickViewers = getURLParam("showKickViewers", true);
|
||||
|
||||
const kickAvatars = new Map();
|
||||
const kick7TVEmojis = new Map();
|
||||
const kickSubBadges = [];
|
||||
|
||||
const kickWebSocketURL = 'wss://ws-us2.pusher.com/app/32cbd69e4b950bf97679?protocol=7&client=js&version=8.4.0&flash=false';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// KICK EVENTS HANDLERS
|
||||
|
||||
const kickMessageHandlers = {
|
||||
|
||||
/*'Kick.ChatMessage': (response) => {\
|
||||
kickChatMessage(response.data);
|
||||
},*/
|
||||
|
||||
'Kick.Follow': (response) => {
|
||||
kickFollowMessage(response.data);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (showKick) {
|
||||
|
||||
const kickStatistics = `
|
||||
<div class="platform" id="kick" style="display: none;">
|
||||
<img src="js/modules/kick/images/logo-kick.svg" alt="">
|
||||
<span class="viewers"><i class="fa-solid fa-user"></i> <span>0</span></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.querySelector('#statistics').insertAdjacentHTML('beforeend', kickStatistics);
|
||||
|
||||
if (showKickViewers == true) { document.querySelector('#statistics #kick').style.display = ''; }
|
||||
|
||||
console.debug('[Kick][Debug] DOMContentLoaded fired');
|
||||
|
||||
registerPlatformHandlersToStreamerBot(kickMessageHandlers, '[Kick][SB1]');
|
||||
|
||||
kickConnection();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// -----------------------
|
||||
// KICK CONNECT HANDLER
|
||||
|
||||
async function kickConnection() {
|
||||
if (!kickUserName) return;
|
||||
|
||||
const kickMaxTries = 20;
|
||||
const kickReconnectDelay = 10000;
|
||||
let retryCount = 0;
|
||||
|
||||
|
||||
|
||||
async function connect() {
|
||||
try {
|
||||
const kickUserInfo = await kickGetUserInfo(kickUserName);
|
||||
const kickUserId = kickUserInfo.user_id;
|
||||
|
||||
if (!kickUserInfo || !kickUserInfo.chatroom || !kickUserInfo.chatroom.id) {
|
||||
throw new Error('Chatroom ID not found');
|
||||
}
|
||||
|
||||
console.debug(`[Kick] User info for ${kickUserName}!`, kickUserInfo);
|
||||
|
||||
const kickChatRoomId = kickUserInfo.chatroom.id;
|
||||
|
||||
if (!kickChatRoomId) {
|
||||
console.error(`[Kick] Could not find chatroom id for ${kickUserName}!`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(`[Kick] Chatroom for ${kickUserName} Found! (ID: ${kickChatRoomId})`);
|
||||
|
||||
kickSubBadges.push(...kickUserInfo.subscriber_badges);
|
||||
|
||||
const kickWebSocket = new WebSocket(kickWebSocketURL);
|
||||
|
||||
kickWebSocket.onopen = () => {
|
||||
kickConnectionState = true;
|
||||
retryCount = 0;
|
||||
|
||||
console.debug(`[Kick] Connected to Kick!`);
|
||||
notifySuccess({
|
||||
title: 'Connected to Kick',
|
||||
text: `User set to <strong>${kickUserName}</strong>.`
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Getting 7TV User Emotes and Global Emotes
|
||||
(async () => {
|
||||
const kick7TVEmotes = await getKick7TVEmotes(kickUserId);
|
||||
if (kick7TVEmotes != null) {
|
||||
kick7TVEmotes.forEach(emote => {
|
||||
kick7TVEmojis.set(emote.name, emote.url);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
|
||||
|
||||
if (showKickViewers === true) {
|
||||
setInterval(() => {
|
||||
kickGetUserInfo(kickUserName).then(data => {
|
||||
kickUpdateStatistics(data);
|
||||
});
|
||||
}, 15000);
|
||||
}
|
||||
};
|
||||
|
||||
kickWebSocket.onmessage = (response) => {
|
||||
const data = JSON.parse(response.data);
|
||||
const kickData = JSON.parse(data.data);
|
||||
const kickEvent = data.event.split('\\').pop();
|
||||
|
||||
console.debug(`[Kick] ${kickEvent}`, kickData);
|
||||
|
||||
if (data.event === 'pusher:connection_established') {
|
||||
|
||||
console.debug(`[Kick][Pusher] Connection established! (ID:${kickData.socket_id})`);
|
||||
|
||||
const channels = [
|
||||
`chatroom_${kickChatRoomId}`,
|
||||
`chatrooms.${kickChatRoomId}`,
|
||||
`chatrooms.${kickChatRoomId}.v2`,
|
||||
`predictions-channel-${kickChatRoomId}`
|
||||
];
|
||||
|
||||
channels.forEach(channel => {
|
||||
kickWebSocket.send(JSON.stringify({
|
||||
event: 'pusher:subscribe',
|
||||
data: { channel }
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
if (data.event === "pusher:ping") {
|
||||
kickWebSocket.send(JSON.stringify({
|
||||
event: "pusher:pong",
|
||||
data: {}
|
||||
}));
|
||||
}
|
||||
|
||||
switch (kickEvent) {
|
||||
case 'ChatMessageEvent': kickChatMessage(kickData); break;
|
||||
case 'SubscriptionEvent': kickSubMessage(kickData); break;
|
||||
case 'GiftedSubscriptionsEvent': kickGiftMessage(kickData); break;
|
||||
case 'RewardRedeemedEvent': kickRewardRedemption(kickData); break;
|
||||
case 'StreamHostEvent': kickRaidMessage(kickData); break;
|
||||
case 'MessageDeletedEvent': kickChatMessageDeleted(kickData); break;
|
||||
case 'UserBannedEvent': kickUserBanned(kickData); break;
|
||||
case 'ChatroomClearEvent': kickChatClearMessages(); break;
|
||||
}
|
||||
};
|
||||
|
||||
kickWebSocket.onclose = (event) => {
|
||||
setTimeout(connect, kickReconnectDelay);
|
||||
|
||||
/*console.warn(`[Kick] WebSocket closed (code: ${event.code})`);
|
||||
|
||||
if (retryCount < kickMaxTries) {
|
||||
retryCount++;
|
||||
notifyError({
|
||||
title: 'Kick Disconnected',
|
||||
text: `Retrying in ${kickReconnectDelay / 1000}s (${retryCount}/${kickMaxTries})`
|
||||
});
|
||||
setTimeout(connect, kickReconnectDelay);
|
||||
} else {
|
||||
notifyError({
|
||||
title: 'Kick Reconnect Failed',
|
||||
text: `Maximum retries (${kickMaxTries}) reached.`
|
||||
});
|
||||
}*/
|
||||
};
|
||||
|
||||
kickWebSocket.onerror = (error) => {
|
||||
console.error('[Kick] WebSocket error:', error);
|
||||
kickWebSocket.close();
|
||||
};
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
setTimeout(connect, kickReconnectDelay);
|
||||
|
||||
/*console.error(`[Kick] Failed to connect: ${error.message}`);
|
||||
|
||||
if (retryCount < kickMaxTries) {
|
||||
retryCount++;
|
||||
notifyError({
|
||||
title: 'Kick Connection Error',
|
||||
text: `Retrying in ${kickReconnectDelay / 1000}s (${retryCount}/${kickMaxTries})`
|
||||
});
|
||||
setTimeout(connect, kickReconnectDelay);
|
||||
} else {
|
||||
notifyError({
|
||||
title: 'Kick Reconnect Failed',
|
||||
text: `Maximum retries (${kickMaxTries}) reached.`
|
||||
});
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
return await connect();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// KICK UTILITY FUNCTIONS
|
||||
|
||||
async function kickChatMessage(data) {
|
||||
|
||||
if (showKickMessages == false) return;
|
||||
if (ignoreUserList.includes(data.sender.username.toLowerCase())) return;
|
||||
if (data.content.startsWith("!") && excludeCommands == true) return;
|
||||
|
||||
const template = chatTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.id;
|
||||
const userId = data.sender.id;
|
||||
const userSlug = data.sender.slug;
|
||||
|
||||
const {
|
||||
'first-message': firstMessage,
|
||||
'shared-chat': sharedChat,
|
||||
|
||||
header,
|
||||
timestamp,
|
||||
platform,
|
||||
badges,
|
||||
avatar,
|
||||
pronouns: pronoun,
|
||||
user,
|
||||
|
||||
reply,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['kick', 'chat'];
|
||||
|
||||
if (userSlug == kickUserName) classes.push('streamer');
|
||||
|
||||
const [avatarImage, messageHTML, badgesHTML] = await Promise.all([
|
||||
getKickAvatar(data.sender.slug),
|
||||
getKickEmotes(data.content),
|
||||
getKickBadges(data.sender.identity.badges),
|
||||
]);
|
||||
|
||||
header.remove();
|
||||
firstMessage.remove();
|
||||
|
||||
user.style.color = data.sender.identity.color;
|
||||
user.innerHTML = `<strong>${data.sender.username}</strong>`;
|
||||
message.innerHTML = messageHTML;
|
||||
|
||||
if (showAvatar) avatar.innerHTML = `<img src="${avatarImage}">`; else avatar.remove();
|
||||
if (showBadges) {
|
||||
if (!badgesHTML) { badges.remove(); }
|
||||
else { badges.innerHTML = badgesHTML; }
|
||||
}
|
||||
else { badges.remove(); }
|
||||
|
||||
if (data.type == "reply") {
|
||||
classes.push('reply');
|
||||
var replyHTML = await getKickEmotes(data.metadata.original_message.content);
|
||||
reply.insertAdjacentHTML('beforeend', ` <strong>${data.metadata.original_sender.username}:</strong> ${replyHTML}`);
|
||||
}
|
||||
else { reply.remove(); }
|
||||
|
||||
sharedChat.remove();
|
||||
pronoun.remove();
|
||||
|
||||
addMessageItem('kick', clone, classes, userSlug, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kickFollowMessage(data) {
|
||||
|
||||
if (showKickFollows == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.user.login.toLowerCase();
|
||||
//const userId = data.userName.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['kick', 'follow'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
//user.innerHTML = `<strong>${data.userName}</strong>`;
|
||||
|
||||
action.innerHTML = ` followed you`;
|
||||
|
||||
value.remove()
|
||||
|
||||
message.remove();
|
||||
|
||||
addEventItem('kick', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kickSubMessage(data) {
|
||||
|
||||
if (showKickSubs == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.username.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['kick', 'sub'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.username}</strong>`;
|
||||
|
||||
action.innerHTML = ` subscribed for `;
|
||||
|
||||
var months = data.months > 1 ? 'months' : 'month';
|
||||
|
||||
value.innerHTML = `<strong>${data.months} ${months}</strong>`;
|
||||
|
||||
message.remove();
|
||||
|
||||
addEventItem('kick', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kickGiftMessage(data) {
|
||||
|
||||
if (showKickGiftedSubs == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.gifter_username.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['kick', 'gift'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.gifter_username}</strong>`;
|
||||
|
||||
var giftedLength = data.gifted_usernames.length;
|
||||
|
||||
if (giftedLength > 1 && showKickMassGiftedSubs == true) {
|
||||
action.innerHTML = ` gifted <strong>${giftedLength} subs</strong> to the Community`;
|
||||
message.innerHTML = `They've gifted a total of <strong>${data.gifter_total} subs</strong>`;
|
||||
value.remove();
|
||||
|
||||
if (showKickGiftedSubsUserTrain == true) {
|
||||
for (recipients of data.gifted_usernames) {
|
||||
kickGiftSingleSub(data.gifter_username, recipients);
|
||||
}
|
||||
}
|
||||
|
||||
addEventItem('kick', clone, classes, userId, messageId);
|
||||
}
|
||||
else {
|
||||
kickGiftSingleSub(data.gifter_username, data.gifted_usernames[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kickGiftSingleSub(gifter, recipient) {
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = gifter.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['kick', 'gift'];
|
||||
|
||||
header.remove();
|
||||
message.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${gifter}</strong>`;
|
||||
|
||||
action.innerHTML = ` gifted a subscription to `;
|
||||
|
||||
value.innerHTML = `<strong>${recipient}</strong>`;
|
||||
|
||||
addEventItem('kick', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kickRewardRedemption(data) {
|
||||
|
||||
if (showKickRewardRedemptions == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.username.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['kick', 'reward'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.username}</strong>`;
|
||||
action.innerHTML = ` redeemed `;
|
||||
value.innerHTML = `<strong>${data.reward_title}</strong>`;
|
||||
|
||||
var userInput = data.user_input ? `- ${data.user_input}` : '';
|
||||
message.innerHTML = `${userInput}`;
|
||||
|
||||
addEventItem('kick', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function kickRaidMessage(data) {
|
||||
|
||||
if (showKickRaids == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.host_username.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['kick', 'raid'];
|
||||
|
||||
header.remove();
|
||||
message.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.host_username}</strong>`;
|
||||
|
||||
var viewers = data.number_viewers > 1 ? 'viewers' : 'viewer';
|
||||
action.innerHTML = ` hosted the channel with `;
|
||||
value.innerHTML = `<strong>${data.number_viewers} ${viewers}</strong>`;
|
||||
|
||||
addEventItem('kick', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function kickChatMessageDeleted(data) {
|
||||
document.getElementById(data.message.id)?.remove();
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kickUserBanned(data) {
|
||||
chatContainer.querySelectorAll(`[data-user="${data.user.slug}"]`).forEach(element => {
|
||||
element.remove();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kickChatClearMessages() {
|
||||
chatContainer.querySelectorAll(`.item.kick`).forEach(element => {
|
||||
element.remove();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kickUpdateStatistics(data) {
|
||||
if (showPlatformStatistics == false || showKickViewers == false) return;
|
||||
if (data.livestream == null) { }
|
||||
else {
|
||||
const viewers = DOMPurify.sanitize(data.livestream.viewer_count);
|
||||
document.querySelector('#statistics #kick .viewers span').textContent = formatNumber(viewers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kickGetUserInfo(user) {
|
||||
const response = await fetch( `https://kick.com/api/v2/channels/${user}` );
|
||||
|
||||
if (response.status === 404) {
|
||||
console.error("[Kick] User was not found!");
|
||||
return 404;
|
||||
}
|
||||
else {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
async function getKickAvatar(user) {
|
||||
if (!showAvatar) return;
|
||||
|
||||
const DEFAULT_AVATAR = 'https://kick.com/img/default-profile-pictures/default2.jpeg';
|
||||
|
||||
if (kickAvatars.has(user)) {
|
||||
console.debug(`[Kick] Kick avatar found for ${user}!`);
|
||||
return kickAvatars.get(user);
|
||||
}
|
||||
|
||||
console.debug(`[Kick] Kick avatar not found for ${user}! Trying to get it...`);
|
||||
|
||||
try {
|
||||
const response = await kickGetUserInfo(user);
|
||||
const rawPic = response?.user?.profile_pic;
|
||||
|
||||
const avatarUrl = (typeof rawPic === "string" && rawPic)
|
||||
? rawPic.replace(/fullsize\.webp$/, "medium.webp")
|
||||
: DEFAULT_AVATAR;
|
||||
|
||||
kickAvatars.set(user, avatarUrl);
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
console.warn(`[Kick] Error getting Kick avatar for ${user}:`, error);
|
||||
return DEFAULT_AVATAR;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getKickEmotes(text) {
|
||||
var message = await parseKickEmojis(text);
|
||||
message = await parseKick7TVEmotes(message);
|
||||
return message;
|
||||
}
|
||||
|
||||
async function parseKickEmojis(content) {
|
||||
const message = content;
|
||||
const messagewithemotes = message.replace(/\[emote:(\d+):([^\]]+)\]/g, (_, id, name) => {
|
||||
return `<img src="https://files.kick.com/emotes/${id}/fullsize" alt="${name}" class="emote" >`;
|
||||
});
|
||||
|
||||
return messagewithemotes;
|
||||
}
|
||||
|
||||
async function parseKick7TVEmotes(text) {
|
||||
const words = text.split(/\s+/);
|
||||
|
||||
const parsedWords = words.map(word => {
|
||||
if (kick7TVEmojis.has(word)) {
|
||||
const url = kick7TVEmojis.get(word);
|
||||
return `<img src="${url}" alt="${word}" class="emote" />`;
|
||||
}
|
||||
return word;
|
||||
});
|
||||
|
||||
return parsedWords.join(' ');
|
||||
}
|
||||
|
||||
|
||||
async function getKick7TVEmotes(userId) {
|
||||
const userSet = await fetch(`https://7tv.io/v3/users/kick/${userId}`);
|
||||
|
||||
if (userSet.status === 404) {
|
||||
console.debug("[Kick] 7TV Profile based on this Kick user was not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
const userEmojis = await userSet.json();
|
||||
|
||||
const gettingAllKick7TVEmotes = userEmojis?.emote_set?.emotes?.map(emote => ({
|
||||
name: emote.name,
|
||||
id: emote.id,
|
||||
url: `https://cdn.7tv.app/emote/${emote.id}/1x.webp`
|
||||
})) || [];
|
||||
|
||||
const globalSet = await fetch(`https://7tv.io/v3/emote-sets/global`);
|
||||
const globalEmojis = await globalSet.json();
|
||||
|
||||
const gettingAllGlobal7TVEmotes = globalEmojis?.emotes?.map(emote => ({
|
||||
name: emote.name,
|
||||
id: emote.id,
|
||||
url: `https://cdn.7tv.app/emote/${emote.id}/1x.webp`
|
||||
})) || [];
|
||||
|
||||
const SevenTVEmotesFusion = [...gettingAllKick7TVEmotes, ...gettingAllGlobal7TVEmotes];
|
||||
|
||||
if (SevenTVEmotesFusion != null) {
|
||||
console.debug("[Kick] Getting all Kick's user 7TV Emojis + Globals", SevenTVEmotesFusion);
|
||||
|
||||
SevenTVEmotesFusion.forEach(emote => {
|
||||
kick7TVEmojis.set(emote.name, emote.url);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getKickBadges(badges) {
|
||||
const badgesArray = [];
|
||||
|
||||
badges.forEach(badge => {
|
||||
if (badge.type === 'subscriber') {
|
||||
|
||||
const targetMonths = badge.count;
|
||||
|
||||
// Sort badges by months
|
||||
const eligibleBadges = kickSubBadges
|
||||
.filter(badge => badge.months <= targetMonths)
|
||||
.sort((a, b) => b.months - a.months); // sorts from highest to lowest
|
||||
|
||||
badgesArray.push(`<img src="${eligibleBadges[0]?.badge_image?.src || 'icons/badges/kick-subscriber.svg'}" class="badge">`);
|
||||
}
|
||||
else {
|
||||
badgesArray.push(`<img src="js/modules/kick/images/badge-${badge.type}.svg" class="badge">`);
|
||||
}
|
||||
});
|
||||
|
||||
return badgesArray.join(' ');
|
||||
}
|
38
js/modules/kofi/images/logo-kofi.svg
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 800 800">
|
||||
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
mask: url(#mask);
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: #202020;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: #ff5a16;
|
||||
}
|
||||
</style>
|
||||
<mask id="mask" x="83" y="145.9" width="632" height="507.1" maskUnits="userSpaceOnUse">
|
||||
<g id="mask0_1_219">
|
||||
<path class="st0" d="M715,145.9H83v507.1h632V145.9Z"/>
|
||||
</g>
|
||||
</mask>
|
||||
</defs>
|
||||
<g class="st1">
|
||||
<g>
|
||||
<path class="st0" d="M335.7,653c-92,0-166.8-41.1-210.7-115.9-38.8-65.5-42-136.4-42-215.7s14.1-87.9,40.9-118.3c24.6-27.9,59.4-46,98-51,45.9-5.8,102.9-6.3,162.2-6.3,96.5,0,123.7,1.2,161.7,5,50.5,5,93,23.9,122.9,54.5,30.3,31.1,46.4,72.7,46.4,120.3v9.6c0,81.2-54.3,149.1-129.9,167.6-5.6,13.3-12.6,26.6-20.9,39.6l-.2.3c-26.6,41.2-89.3,110.3-209.2,110.3h-19.1,0Z"/>
|
||||
<path class="st0" d="M541.8,190.7c-35.9-3.6-60.9-4.8-157.7-4.8s-114.7.6-157.1,6c-56.2,7.2-104,50.2-104,129.7s4.2,141,36.4,195.4c36.4,62.1,97.4,96.2,176.3,96.2h19.1c96.8,0,149.4-51.4,175.7-92,11.4-17.9,19.7-35.8,25.1-53.8,68.7-6,119.5-62.7,119.5-132v-9.6c0-74.7-49-126.7-133.2-135h0Z"/>
|
||||
<path class="st2" d="M123,321.5c0-79.5,47.8-122.5,104-129.7,42.4-5.4,95-6,157.1-6,96.8,0,121.9,1.2,157.7,4.8,84.3,8.4,133.2,60.3,133.2,135v9.6c0,69.3-50.8,126.1-119.5,132-5.4,17.9-13.7,35.8-25.1,53.8-26.3,40.6-78.9,92-175.7,92h-19.1c-78.9,0-139.8-34.1-176.3-96.2-32.3-54.4-36.4-114.7-36.4-195.4"/>
|
||||
<path class="st0" d="M167.8,322.1c0,77.1,4.8,126.7,29.9,172.7,28.7,53.2,80.7,73.5,139.8,73.5h18.5c77.7,0,115.3-37.6,136.2-70.5,10.2-16.7,19.1-35.2,23.9-58.6l3.6-14.9h21.5c47.8,0,89-38.8,89-88.4v-9c0-55.6-34.6-84.9-95-92-34.1-3-54.4-4.2-151.2-4.2s-111.7.6-147,6c-49.6,7.2-69.3,35.2-69.3,85.4"/>
|
||||
<path class="st2" d="M519.7,363.3c0,7.2,5.4,12.6,14.9,12.6,30.5,0,47.2-17.3,47.2-46s-16.7-46.6-47.2-46.6-14.9,5.4-14.9,12.6v67.5h0Z"/>
|
||||
<path class="st3" d="M226.3,359.8c0,35.2,19.7,65.7,44.8,89.6,16.7,16.1,43,32.9,60.9,43.6,5.4,3,10.8,4.8,16.7,4.8s13.1-1.8,17.9-4.8c17.9-10.8,44.2-27.5,60.3-43.6,25.7-23.9,45.4-54.4,45.4-89.6s-28.7-72.3-69.9-72.3-41.2,12.6-53.8,29.9c-11.4-17.3-28.7-29.9-53.2-29.9-41.8,0-69.3,34-69.3,72.3"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
11
js/modules/kofi/module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
#chat .event.kofi .message {
|
||||
background: rgba(114, 165, 242,0.75);
|
||||
}
|
||||
|
||||
#chat .event.kofi .header {
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
}
|
||||
#chat .event.kofi .header img {
|
||||
height: 128px;
|
||||
}
|
192
js/modules/kofi/module.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/* --------------------- */
|
||||
/* KOFI MODULE VARIABLES */
|
||||
/* --------------------- */
|
||||
|
||||
const showKofi = getURLParam("showKofi", true);
|
||||
|
||||
const showKofiSubscriptions = getURLParam("showKofiSubscriptions", true);
|
||||
const showKofiDonations = getURLParam("showKofiDonations", true);
|
||||
const showKofiOrders = getURLParam("showKofiOrders", true);
|
||||
|
||||
|
||||
// KOFI EVENTS HANDLERS
|
||||
|
||||
const kofiMessageHandlers = {
|
||||
'Kofi.Donation': (response) => {
|
||||
kofiDonationMessage(response.data);
|
||||
},
|
||||
'Kofi.Subscription': (response) => {
|
||||
kofiSubMessage(response.data);
|
||||
},
|
||||
'Kofi.Resubscription': (response) => {
|
||||
kofiReSubMessage(response.data);
|
||||
},
|
||||
'Kofi.ShopOrder': (response) => {
|
||||
kofiOrderMessage(response.data);
|
||||
},
|
||||
};
|
||||
|
||||
if (showKofi) {
|
||||
registerPlatformHandlersToStreamerBot(kofiMessageHandlers, '[Ko-Fi]');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// KOFI EVENT FUNCTIONS
|
||||
|
||||
async function kofiDonationMessage(data) {
|
||||
|
||||
if (kofiDonationMessage == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['kofi', 'donation'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.from}</strong>`;
|
||||
action.innerHTML = ` donated `;
|
||||
|
||||
var money = formatCurrency(data.amount,data.currency);
|
||||
value.innerHTML = `<strong>${money}</strong>`;
|
||||
|
||||
if (data.message) message.innerHTML = `${data.message}`;
|
||||
|
||||
addEventItem('kofi', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kofiSubMessage(data) {
|
||||
|
||||
if (showKofiSubscriptions == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['kofi', 'sub'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.from}</strong>`;
|
||||
action.innerHTML = ` just subscribed `;
|
||||
|
||||
var money = formatCurrency(data.amount,data.currency);
|
||||
value.innerHTML = `<strong>(${money})</strong>`;
|
||||
|
||||
if (data.message) message.innerHTML = `${data.message}`;
|
||||
|
||||
addEventItem('kofi', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kofiReSubMessage(data) {
|
||||
|
||||
if (showKofiSubscriptions == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['kofi', 'sub'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.from}</strong>`;
|
||||
action.innerHTML = ` just resubscribed `;
|
||||
|
||||
var money = formatCurrency(data.amount,data.currency);
|
||||
value.innerHTML = `<strong>(${money}) ${data.tier ? '(Tier '+data.tier+')' : ''}</strong>`;
|
||||
|
||||
if (data.message) message.innerHTML = `${data.message}`;
|
||||
|
||||
addEventItem('kofi', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function kofiOrderMessage(data) {
|
||||
|
||||
if (showKofiOrders == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['kofi', 'sub'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.from}</strong>`;
|
||||
action.innerHTML = ` just ordered `;
|
||||
|
||||
var money = '';
|
||||
if (data.amount == 0) money = 'Free';
|
||||
else money = formatCurrency(data.amount,data.currency);
|
||||
|
||||
value.innerHTML = `<strong>${data.items.length} ${data.items.length > 1 ? 'item' : 'items'} (${money})</strong>`;
|
||||
|
||||
if (data.message) message.innerHTML = `${data.message}`;
|
||||
|
||||
addEventItem('kofi', clone, classes, userId, messageId);
|
||||
}
|
12
js/modules/patreon/images/logo-patreon.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 800 800">
|
||||
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0" d="M694.3,279.9c-.8,47.8-15.1,88-42.8,120.7-26.8,33.5-63.3,56.2-109.4,67.9-26.8,5.9-50.7,13.4-71.7,22.6-21.8,10.1-41.1,26-57.9,47.8-13.4,19.3-25.6,42.4-36.5,69.2-12.6,31-28.1,58.7-46.5,83-18.5,25.2-44.9,38.1-79.2,39-44.5-3.4-77.2-25.6-98.1-66.7-21-41.9-34-88.9-39-140.9-5.9-52-8.4-94.3-7.5-127,0-57,10.5-110.3,31.4-159.7,21.8-49.5,59.1-87.6,111.9-114.5,46.1-20.9,96.4-32.7,150.9-35.2,55.3-2.5,106.9,4.2,154.7,20.1,38.6,12.6,71.3,34,98.1,64.1,26,31,39.8,67.5,41.5,109.4Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 813 B |
3
js/modules/patreon/module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#chat .event.patreon .message {
|
||||
background: rgba(255,89,0,0.75);
|
||||
}
|
60
js/modules/patreon/module.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/* ------------------------- */
|
||||
/* PATREON MODULE VARIABLES */
|
||||
/* ------------------------- */
|
||||
|
||||
const showPatreon = getURLParam("showPatreon", true);
|
||||
|
||||
const showPatreonMemberships = getURLParam("showPatreonMemberships", true);
|
||||
|
||||
// PATREON EVENTS HANDLERS
|
||||
|
||||
const patreonHandlers = {
|
||||
'Patreon.PledgeCreated': (response) => {
|
||||
patreonMemberships(response.data);
|
||||
},
|
||||
};
|
||||
|
||||
if (showPatreon) {
|
||||
registerPlatformHandlersToStreamerBot(patreonHandlers, '[Patreon]');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// PATREON EVENTS FUNCTIONS
|
||||
|
||||
async function patreonMemberships(data) {
|
||||
|
||||
if (showPatreonMemberships == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['patreon', 'membership'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
var money = (data.attributes.will_pay_amount_cents / 100).toFixed(2);
|
||||
|
||||
user.innerHTML = `<strong>${data.attributes.full_name}</strong>`;
|
||||
action.innerHTML = ` donated `;
|
||||
value.innerHTML = `<strong>${money}</strong>`;
|
||||
|
||||
message.remove();
|
||||
|
||||
addEventItem('patreon', clone, classes, userId, messageId);
|
||||
}
|
82
js/modules/streamelements/images/logo-streamelements.svg
Normal file
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 800 800">
|
||||
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: url(#linear-gradient2);
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
mask: url(#mask);
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: url(#linear-gradient1);
|
||||
}
|
||||
|
||||
.st4 {
|
||||
fill: url(#linear-gradient4);
|
||||
}
|
||||
|
||||
.st5 {
|
||||
fill: #ff4800;
|
||||
}
|
||||
|
||||
.st6 {
|
||||
fill: url(#linear-gradient3);
|
||||
}
|
||||
|
||||
.st7 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="linear-gradient" x1="399.1" y1="810.1" x2="399.1" y2="-83.5" gradientTransform="translate(0 800) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".3" stop-color="#00adff"/>
|
||||
<stop offset="1" stop-color="#1542ff"/>
|
||||
</linearGradient>
|
||||
<mask id="mask" x="152.6" y="181.1" width="493" height="481.8" maskUnits="userSpaceOnUse">
|
||||
<g id="mask0_131_34">
|
||||
<path class="st1" d="M399.1,662.5c133,0,240.9-107.8,240.9-240.7s-107.8-240.7-240.9-240.7-240.9,107.8-240.9,240.7,107.8,240.7,240.9,240.7Z"/>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="linear-gradient1" x1="399" y1="377" x2="399" y2="55.8" gradientTransform="translate(0 800) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fce3ad"/>
|
||||
<stop offset="0" stop-color="#fce2a4"/>
|
||||
<stop offset=".2" stop-color="#fce08b"/>
|
||||
<stop offset=".3" stop-color="#fcdd64"/>
|
||||
<stop offset=".5" stop-color="#fcda32"/>
|
||||
<stop offset=".7" stop-color="#f9bc22"/>
|
||||
<stop offset="1" stop-color="#f57700"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient2" x1="399.1" y1="294.8" x2="399.1" y2="112.3" gradientTransform="translate(0 800) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fcd619"/>
|
||||
<stop offset=".3" stop-color="#f9b811"/>
|
||||
<stop offset="1" stop-color="#f57700"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient3" x1="399.1" y1="203.5" x2="399.1" y2="119.4" gradientTransform="translate(0 800) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#f57700"/>
|
||||
<stop offset="1" stop-color="#ff4800"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient4" x1="399.1" y1="704.2" x2="399.1" y2="279.8" gradientTransform="translate(0 800) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#e4e4e4"/>
|
||||
<stop offset=".9" stop-color="#b6b8b8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path class="st7" d="M470.6,150l15.3,29.8c101.5,36.4,171.2,134.3,171.2,242.7s-115.7,257.8-258,257.8-257.9-115.6-257.9-257.8,69.7-206.3,171.2-242.7l15.3-29.7c-122.9,32.1-210.4,144-210.4,272.4s126.5,281.7,281.8,281.7,281.8-126.4,281.8-281.7-87.5-240.3-210.4-272.4Z"/>
|
||||
<g class="st2">
|
||||
<g>
|
||||
<path class="st3" d="M640.5,416.1c-1.4,18.3-29.7,94.4-64.5,94.4s-17.3-20.7-19.6-36c-6.3,24.5-49.5,77.5-81.5,77.5-58.3,0-71.8-122.8-75.8-139.2-4,16.3-17.5,139.2-75.8,139.2s-75.3-52.9-81.5-77.5c-2.3,15.4-5.4,36-19.6,36-34.8,0-63.1-76.1-64.5-94.4-7.5,11-7.3,55.5,3.7,84,37,95.1,129.5,162.7,237.6,162.7s200.6-67.6,237.6-162.7c11.1-28.5,11.2-73,3.7-84h.1Z"/>
|
||||
<path class="st0" d="M399.1,552.2c9.6,17.6,30.5,44,66.3,44s63.7-36.9,73-52.5c-3,11-9,34.4,10.5,34.4,44.7,0,77-70.6,77-70.6-35.3,90.8-123.6,155.3-226.8,155.3s-191.5-64.5-226.8-155.3c0,0,32.3,70.6,77,70.6s13.4-23.5,10.5-34.4c9.3,15.6,36.3,52.5,73,52.5s56.7-26.4,66.3-44Z"/>
|
||||
<path class="st6" d="M455.9,631.5c-22,0-39.1-6.8-56.8-17.8-17.7,11-34.8,17.8-56.8,17.8-33.5,0-43.7-9.9-55.8-18.9.3,11.5,12.1,27.8,24.2,33.7,27,11.9,57.2,16.6,88.4,16.6s61.4-4.8,88.4-16.6c12.1-5.9,23.8-22.1,24.2-33.7-12.1,9-22.3,18.9-55.8,18.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st5" d="M366.6,304.9l-34.6,73.2c-1.4,3-1.9,6.4-1.4,9.7l11,66.2c.3,2.1,3.5,2.1,3.8,0l5.9-36.1c2.4-15,10.4-28.5,22.3-37.9l-7.1-75.2h0Z"/>
|
||||
<path class="st5" d="M431.7,304.9l34.6,73.2c1.4,3,1.9,6.4,1.4,9.7l-11,66.2c-.3,2.1-3.4,2.1-3.8,0l-5.9-36.1c-2.4-15-10.4-28.5-22.3-37.9l7.1-75.2h0Z"/>
|
||||
<path class="st4" d="M450.2,190l-47.4-91.9c-1.5-3-5.8-3-7.3,0l-47.4,91.9c-2,3.8-2.7,8.1-2.1,12.3l23.9,167.9-3.6,15.7,12.6,34.6c.3.8,1,1.3,1.8,1.3h36.7c.8,0,1.5-.5,1.8-1.3l12.6-34.6-3.6-15.7,23.9-167.9c.6-4.2-.1-8.5-2.1-12.3h.2Z"/>
|
||||
<path class="st5" d="M397.5,342.5l-16.8,41.6c-2.6,6.4-3.1,13.6-1.4,20.3l18,71.9c.5,2,3.3,2,3.8,0l18-71.9c1.7-6.7,1.2-13.8-1.4-20.3l-16.8-41.6c-.6-1.5-2.7-1.5-3.3,0h0Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
3
js/modules/streamelements/module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#chat .event.streamelements .message {
|
||||
background: rgba(39, 0, 255, 0.75);
|
||||
}
|
57
js/modules/streamelements/module.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/* ------------------------------- */
|
||||
/* STREAMELEMENTS MODULE VARIABLES */
|
||||
/* ------------------------------ */
|
||||
|
||||
const showStreamelements = getURLParam("showStreamelements", true);
|
||||
|
||||
const showStreamElementsTips = getURLParam("showStreamElementsTips", true);
|
||||
|
||||
const streamElementsHandlers = {
|
||||
'StreamElements.Tip': (response) => {
|
||||
streamElementsEventMessage(response.data);
|
||||
},
|
||||
};
|
||||
|
||||
if (showStreamelements) {
|
||||
registerPlatformHandlersToStreamerBot(streamElementsHandlers, '[Streamelements]');
|
||||
}
|
||||
|
||||
|
||||
async function streamElementsEventMessage(data) {
|
||||
|
||||
if (showTwitchRewardRedemptions == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['streamelements', 'donation'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
var money = formatCurrency(data.amount,data.currency);
|
||||
|
||||
user.innerHTML = `<strong>${data.username}</strong>`;
|
||||
action.innerHTML = ` donated `;
|
||||
value.innerHTML = `<strong>${money}</strong>`;
|
||||
|
||||
if (data.message) { message.innerHTML = `${data.message}`; }
|
||||
else { message.remove(); }
|
||||
|
||||
|
||||
addEventItem('streamelements', clone, classes, userId, messageId);
|
||||
}
|
14
js/modules/streamlabs/images/logo-streamlabs.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 800 800">
|
||||
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0" d="M239.1,449.1c0-62.2,0-93.2,12.1-117,10.6-20.9,27.6-37.9,48.5-48.5,23.7-12.1,54.8-12.1,117-12.1h88.8c62.2,0,93.2,0,117,12.1,20.9,10.6,37.9,27.6,48.5,48.5,12.1,23.7,12.1,54.8,12.1,117v25.4c0,62.2,0,93.2-12.1,117-10.6,20.9-27.6,37.9-48.5,48.5-23.7,12.1-54.8,12.1-117,12.1h-164.9c-35.5,0-53.3,0-66.8-6.9-11.9-6.1-21.6-15.8-27.7-27.7-6.9-13.6-6.9-31.3-6.9-66.8v-101.5ZM397.6,461.8c0-17.5,14.2-31.7,31.7-31.7s31.7,14.2,31.7,31.7v63.4c0,17.5-14.2,31.7-31.7,31.7s-31.7-14.2-31.7-31.7v-63.4ZM556.1,430.1c-17.5,0-31.7,14.2-31.7,31.7v63.4c0,17.5,14.2,31.7,31.7,31.7s31.7-14.2,31.7-31.7v-63.4c0-17.5-14.2-31.7-31.7-31.7Z"/>
|
||||
<path class="st0" d="M349.8,175.7c2.2,17.4-10.2,33.2-27.6,35.4-9.6,1.2-17.3,2.8-24.1,4.9-54.7,17-97.6,59.9-114.6,114.6-2.1,6.8-3.7,14.5-4.9,24.1-2.2,17.4-18,29.7-35.4,27.6-17.4-2.2-29.7-18-27.6-35.4,1.6-12.5,3.8-24,7.3-35.1,23.2-74.6,81.7-133,156.3-156.3,11.1-3.5,22.6-5.8,35.1-7.3,17.4-2.2,33.2,10.2,35.4,27.6Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
3
js/modules/streamlabs/module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#chat .event.streamlabs .message {
|
||||
background: rgba(128,245,210,0.75);
|
||||
}
|
59
js/modules/streamlabs/module.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/* ---------------------------- */
|
||||
/* STREAMLABS MODULE VARIABLES */
|
||||
/* ---------------------------- */
|
||||
|
||||
const showStreamlabs = getURLParam("showStreamlabs", true);
|
||||
|
||||
const showStreamlabsDonations = getURLParam("showStreamlabsDonations", true);
|
||||
|
||||
const streamlabsHandlers = {
|
||||
'Streamlabs.Donation' : (response) => {
|
||||
streamLabsEventMessage(response.data);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
if (showStreamlabs) {
|
||||
registerPlatformHandlersToStreamerBot(streamlabsHandlers, '[Streamlabs]');
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function streamLabsEventMessage(data) {
|
||||
|
||||
if (showStreamlabsDonations == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['streamlabs', 'donation'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
var money = formatCurrency(data.amount,data.currency);
|
||||
|
||||
user.innerHTML = `<strong>${data.from}</strong>`;
|
||||
action.innerHTML = ` donated `;
|
||||
value.innerHTML = `<strong>${money}</strong>`;
|
||||
|
||||
if (data.message) { message.innerHTML = `${data.message}`; }
|
||||
else { message.remove(); }
|
||||
|
||||
|
||||
addEventItem('streamlabs', clone, classes, userId, messageId);
|
||||
}
|
12
js/modules/tiktok/images/logo-tiktok.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 800 800">
|
||||
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0" d="M682.3,351.7c-55.5.1-109.6-17.2-154.7-49.5v225.3c0,85.4-53.1,161.9-133.1,191.8-80,29.9-170.2,7.1-226.3-57.4-56.1-64.4-66.3-156.9-25.7-232.1,40.7-75.1,123.7-117.1,208.3-105.4v113.3c-38.8-12.2-81,1.9-104.7,35-23.7,33.1-23.4,77.6.6,110.4,24.1,32.8,66.5,46.4,105.1,33.7s64.8-48.7,64.8-89.4V87.1h110.9c0,9.4.7,18.7,2.3,27.9,7.8,41.6,32.4,78.2,67.9,101.1,25,16.6,54.4,25.4,84.4,25.4v110.1Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 730 B |
30
js/modules/tiktok/module.css
Normal file
@@ -0,0 +1,30 @@
|
||||
#chat .event.tiktok .message {
|
||||
background: rgba(255,0,80,0.75);
|
||||
}
|
||||
|
||||
#chat .item.tiktok .platform .hidden-platform {
|
||||
background: rgba(255,0,80,1);
|
||||
}
|
||||
|
||||
#chat .item.tiktok span.badge {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
vertical-align: top;
|
||||
color: #FFF;
|
||||
background: #121212;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
|
||||
#chat .event.tiktok.gift .value img { vertical-align: sub; }
|
||||
|
||||
#chat.horizontal .item.tiktok .info .badges {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
#statistics .platform#tiktok { background: #ff0050; }
|
435
js/modules/tiktok/module.js
Normal file
@@ -0,0 +1,435 @@
|
||||
/* ----------------------- */
|
||||
/* TIKTOK MODULE VARIABLES */
|
||||
/* ----------------------- */
|
||||
|
||||
const showTiktok = getURLParam("showTiktok", true);
|
||||
|
||||
const showTikTokMessages = getURLParam("showTikTokMessages", true);
|
||||
const showTikTokFollows = getURLParam("showTikTokFollows", true);
|
||||
const showTikTokLikes = getURLParam("showTikTokLikes", true);
|
||||
const showTikTokGifts = getURLParam("showTikTokGifts", true);
|
||||
const showTikTokSubs = getURLParam("showTikTokSubs", true);
|
||||
const showTikTokStatistics = getURLParam("showTikTokStatistics", true);
|
||||
|
||||
userColors.set('tiktok', new Map());
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (showTiktok) {
|
||||
const tiktokStatistics = `
|
||||
<div class="platform" id="tiktok" style="display: none;">
|
||||
<img src="js/modules/tiktok/images/logo-tiktok.svg" alt="">
|
||||
<span class="viewers"><i class="fa-solid fa-user"></i> <span>0</span></span>
|
||||
<span class="likes"><i class="fa-solid fa-thumbs-up"></i> <span>0</span></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.querySelector('#statistics').insertAdjacentHTML('beforeend', tiktokStatistics);
|
||||
|
||||
if (showTikTokStatistics == true) { document.querySelector('#statistics #tiktok').style.display = ''; }
|
||||
|
||||
console.debug('[TikTok][Debug] DOMContentLoaded fired');
|
||||
|
||||
tiktokConnection();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// -----------------------
|
||||
// TIKTOK CONNECT HANDLER
|
||||
|
||||
async function tiktokConnection() {
|
||||
const tikfinityWebSocketURL = 'ws://localhost:21213/'; // Replace with real URL
|
||||
const reconnectDelay = 10000; // 10 seconds
|
||||
const maxTries = 20;
|
||||
let retryCount = 0;
|
||||
|
||||
function connect() {
|
||||
const tikfinityWebSocket = new WebSocket(tikfinityWebSocketURL);
|
||||
|
||||
tikfinityWebSocket.onopen = () => {
|
||||
console.debug(`[TikFinity] Connected to TikFinity successfully!`);
|
||||
retryCount = 0; // Reset retry count on success
|
||||
|
||||
notifySuccess({
|
||||
title: 'Connected to TikFinity',
|
||||
text: ``
|
||||
});
|
||||
};
|
||||
|
||||
tikfinityWebSocket.onmessage = (response) => {
|
||||
|
||||
const data = JSON.parse(response.data);
|
||||
const tiktokData = data.data;
|
||||
|
||||
console.debug(`[TikTok] ${data.event}`, data.data);
|
||||
|
||||
switch (data.event) {
|
||||
case 'roomUser' : tiktokUpdateStatistics(tiktokData, 'viewers'); break;
|
||||
case 'like': tiktokLikesMessage(tiktokData); tiktokUpdateStatistics(tiktokData, 'likes'); break;
|
||||
case 'chat': tiktokChatMessage(tiktokData); break;
|
||||
case 'follow': tiktokFollowMessage(tiktokData); break;
|
||||
case 'gift': tiktokGiftMessage(tiktokData); break;
|
||||
case 'subscribe': tiktokSubMessage(tiktokData); break;
|
||||
}
|
||||
};
|
||||
|
||||
tikfinityWebSocket.onclose = (event) => {
|
||||
|
||||
setTimeout(() => {
|
||||
connect();
|
||||
}, reconnectDelay);
|
||||
|
||||
|
||||
/*console.error(`[TikFinity] Disconnected (code: ${event.code})`);
|
||||
|
||||
if (retryCount < maxTries) {
|
||||
retryCount++;
|
||||
console.warn(`[TikFinity] Attempt ${retryCount}/${maxTries} - Reconnecting in ${reconnectDelay / 1000}s...`);
|
||||
|
||||
notifyError({
|
||||
title: 'TikFinity Disconnected',
|
||||
text: `Attempt ${retryCount}/${maxTries} - Reconnecting in ${reconnectDelay / 1000}...`
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
connect();
|
||||
}, reconnectDelay);
|
||||
}
|
||||
else {
|
||||
notifyError({
|
||||
title: 'TikFinity Reconnect Failed',
|
||||
text: `Maximum retries (${maxTries}) reached. Reload ChatRD to try again.<br>(Check DevTools Debug for more info).`
|
||||
});
|
||||
console.error('[TikFinity] Max reconnect attempts reached. Giving up.');
|
||||
}*/
|
||||
};
|
||||
|
||||
tikfinityWebSocket.onerror = (error) => {
|
||||
console.error(`[TikFinity] Connection error:`, error);
|
||||
|
||||
// Force close to trigger onclose and centralize retry logic
|
||||
if (tikfinityWebSocket.readyState !== WebSocket.CLOSED) {
|
||||
tikfinityWebSocket.close();
|
||||
}
|
||||
};
|
||||
|
||||
return tikfinityWebSocket;
|
||||
}
|
||||
|
||||
return connect(); // Returns the initial WebSocket instance
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// TIKTOK UTILITY FUNCTIONS
|
||||
|
||||
async function tiktokChatMessage(data) {
|
||||
|
||||
if (showTikTokMessages == false) return;
|
||||
if (ignoreUserList.includes(data.nickname.toLowerCase())) return;
|
||||
if (data.comment.startsWith("!") && excludeCommands == true) return;
|
||||
|
||||
const template = chatTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.msgId;
|
||||
const userId = data.userId;
|
||||
|
||||
const {
|
||||
'first-message': firstMessage,
|
||||
'shared-chat': sharedChat,
|
||||
|
||||
header,
|
||||
timestamp,
|
||||
platform,
|
||||
badges,
|
||||
avatar,
|
||||
pronouns: pronoun,
|
||||
user,
|
||||
|
||||
reply,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['tiktok', 'chat'];
|
||||
|
||||
if (data.isModerator) classes.push('mod');
|
||||
if (data.isSubscriber) classes.push('sub');
|
||||
|
||||
const [avatarImage, messageHTML, badgesHTML] = await Promise.all([
|
||||
getTikTokAvatar(data),
|
||||
getTikTokEmotes(data),
|
||||
getTikTokBadges(data),
|
||||
]);
|
||||
|
||||
header.remove();
|
||||
firstMessage.remove();
|
||||
|
||||
sharedChat.remove();
|
||||
reply.remove();
|
||||
pronoun.remove();
|
||||
|
||||
if (showAvatar) avatar.innerHTML = `<img src="${avatarImage}">`; else avatar.remove();
|
||||
|
||||
if (showBadges) {
|
||||
if (!badgesHTML) { badges.remove(); }
|
||||
else { badges.innerHTML = badgesHTML; }
|
||||
}
|
||||
else { badges.remove(); }
|
||||
|
||||
var color = await createRandomColor('tiktok', data.uniqueId);
|
||||
|
||||
user.style.color = color;
|
||||
user.innerHTML = `<strong>${data.nickname}</strong>`;
|
||||
message.innerHTML = messageHTML;
|
||||
|
||||
addMessageItem('tiktok', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function tiktokFollowMessage(data) {
|
||||
|
||||
if (showTikTokFollows == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.msgId;
|
||||
const userId = data.userId;
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['tiktok', 'follow'];
|
||||
|
||||
header.remove();
|
||||
message.remove();
|
||||
value.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.nickname}</strong>`;
|
||||
|
||||
action.innerHTML = ` followed you`;
|
||||
|
||||
addEventItem('tiktok', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function tiktokLikesMessage(data) {
|
||||
|
||||
if (showTikTokLikes == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.msgId;
|
||||
const userId = data.userId;
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['tiktok', 'likes'];
|
||||
|
||||
|
||||
var likeCountTotal = parseInt(data.likeCount);
|
||||
|
||||
// Search for Previous Likes from the Same User
|
||||
const previousLikeContainer = chatContainer.querySelector(`div.event.tiktok.likes[data-user="${data.userId}"]`);
|
||||
|
||||
// If found, fetches the previous likes, deletes the element
|
||||
// and then creates a new count with a sum of the like count
|
||||
if (previousLikeContainer) {
|
||||
const likeCountElem = previousLikeContainer.querySelector('.value strong');
|
||||
if (likeCountElem) {
|
||||
var likeCountPrev = parseInt(likeCountElem.textContent);
|
||||
likeCountTotal = Math.floor(likeCountPrev + likeCountTotal);
|
||||
removeItem(previousLikeContainer);
|
||||
}
|
||||
}
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.nickname}</strong>`;
|
||||
action.innerHTML = ` sent you `;
|
||||
|
||||
var likes = likeCountTotal > 1 ? 'likes' : 'like';
|
||||
value.innerHTML = `<strong>${likeCountTotal}</strong> ${likes} ❤️`;
|
||||
|
||||
message.remove();
|
||||
|
||||
addEventItem('tiktok', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function tiktokSubMessage(data) {
|
||||
|
||||
if (showTikTokSubs == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.msgId;
|
||||
const userId = data.userId;
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['tiktok', 'sub'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.nickname}</strong>`;
|
||||
action.innerHTML = ` subscribed for `;
|
||||
|
||||
var months = data.subMonth > 1 ? 'months' : 'month';
|
||||
value.innerHTML = `<strong>${data.subMonth} ${months}</strong>`;
|
||||
|
||||
message.remove();
|
||||
|
||||
addEventItem('tiktok', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function tiktokGiftMessage(data) {
|
||||
|
||||
if (showTikTokGifts == false) return;
|
||||
if (data.giftType === 1 && !data.repeatEnd) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.msgId;
|
||||
const userId = data.userId;
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['tiktok', 'gift'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.nickname}</strong>`;
|
||||
action.innerHTML = ` has sent you `;
|
||||
value.innerHTML = `<strong>x${data.repeatCount} ${data.giftName}</strong> <img src="${data.giftPictureUrl}"> `;
|
||||
|
||||
message.remove();
|
||||
|
||||
addEventItem('tiktok', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function getTikTokEmotes(data) {
|
||||
const {
|
||||
comment: message,
|
||||
emotes,
|
||||
} = data;
|
||||
|
||||
var fullmessage = message;
|
||||
|
||||
if (emotes.length > 0) {
|
||||
emotes.forEach(emote => {
|
||||
var emotetoadd = ` <img src="${emote.emoteImageUrl}" class="emote" data-emote-id="${emote.emoteId}"> `;
|
||||
var position = emote.placeInComment;
|
||||
fullmessage = [fullmessage.slice(0, position), emotetoadd, fullmessage.slice(position)].join('');
|
||||
});
|
||||
}
|
||||
|
||||
return fullmessage;
|
||||
}
|
||||
|
||||
|
||||
async function getTikTokAvatar(data) {
|
||||
const {
|
||||
profilePictureUrl
|
||||
} = data;
|
||||
|
||||
return profilePictureUrl;
|
||||
}
|
||||
|
||||
async function getTikTokBadges(data) {
|
||||
const {
|
||||
isSubscriber,
|
||||
isModerator,
|
||||
} = data;
|
||||
|
||||
let badgesHTML = [
|
||||
isSubscriber && '<span class="badge sub"><i class="fa-solid fa-star"></i></span>',
|
||||
isModerator && '<span class="badge mod"><i class="fa-solid fa-user-gear"></i></span>',
|
||||
].filter(Boolean).join('');
|
||||
|
||||
return badgesHTML;
|
||||
}
|
||||
|
||||
|
||||
async function tiktokUpdateStatistics(data, type) {
|
||||
|
||||
if (showPlatformStatistics == false || showTikTokStatistics == false) return;
|
||||
|
||||
if (type == 'viewers') {
|
||||
const viewers = DOMPurify.sanitize(data.viewerCount);
|
||||
document.querySelector('#statistics #tiktok .viewers span').textContent = formatNumber(viewers);
|
||||
}
|
||||
|
||||
if (type == 'likes') {
|
||||
const likes = DOMPurify.sanitize(data.totalLikeCount);
|
||||
document.querySelector('#statistics #tiktok .likes span').textContent = formatNumber(likes);
|
||||
}
|
||||
|
||||
}
|
13
js/modules/tipeeestream/images/logo-tipeeestream.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 800 800">
|
||||
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0" d="M248,251.9h74.5c1,3.9,1.9,7.5,2.2,11.6.8,10.7,1.1,83.5-1.2,88.5s-3.9,4.3-4.2,4.3h-71.3v162.9c0,1.7,6.5,9.2,8.6,10.6,19.7,12.8,60.8-10.6,65.7-7.2,1.6,3.7,2.1,7.4,2.4,11.4,1.3,18.3,2.5,64-.2,80.9-2.5,15.9-30.3,22.3-44.1,26.1-57.2,16-126.4,18.7-149-47.9-2.2-6.6-6.9-21-6.9-27.1v-209.8h-35.1c-6.2,0-7.1-9.2-7.5-13.8-.9-10.4-1.1-81.4,1.2-86.3s3.9-4.3,4.2-4.3h34.1l3.2-3.2v-70.3c0-3.4,7.3-9.2,10.6-10.7,15.3-6.7,77.3-25.9,93.2-28.2,8.6-1.2,19.7.2,19.7,11.2v101.2h0Z"/>
|
||||
<path class="st0" d="M685.7,122l-263.5.5h0c-18.5,2.7-28.7,16.1-29.4,34.5v483.6c1.9,22.5,12.6,33.2,35.1,35.1,81.6,7,173-5.4,255.7,0,20.6-.4,34.7-14.4,35.1-35.1V157c-.9-20.6-12.6-32.9-33.1-35ZM675.6,510c-3.8,20.5-13.2,36.8-26.2,49.5-51,49.9-158.8,44-216.1,12.9v-71.3s0,0,0,0h0s0,0,0,0h0c0,0,0,0,0,0,3.4-2.6,33.8,12.5,39.8,14.5,24.3,8.1,86,24.7,111.9,5.3,4.2-3.2,7.5-7.3,9.5-12.6,18.8-49.7-58.3-69.1-90.4-87.6-47.6-27.5-77.3-68.9-64.1-126.6,4.6-20.3,14.2-36,27.1-47.9,50.5-46.5,152.3-33.5,208.9-4.9l-25.7,61.7s0,0,0,0h0c-28.8-12.1-105.8-37.1-126.9-5.5-4.1,6.3-6.1,14.7-5,25.9,1.3,13.8,13,24.2,27.4,32.6,10.2,4.8,23.7,10.5,41.5,19.7,2.1,1,4.1,2,5.9,2.9,54.5,29.1,95.4,62.9,82.5,131.5Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
3
js/modules/tipeeestream/module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#chat .event.tipeeestream .message {
|
||||
background: rgba(224,47,68,0.75);
|
||||
}
|
61
js/modules/tipeeestream/module.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/* ----------------------------- */
|
||||
/* TIPEEESTREAM MODULE VARIABLES */
|
||||
/* ----------------------------- */
|
||||
|
||||
const showTipeee = getURLParam("showTipeee", true);
|
||||
|
||||
const showTipeeeDonations = getURLParam("showTipeeeDonations", true);
|
||||
|
||||
// TIPEEESTREAM EVENTS HANDLERS
|
||||
|
||||
const tipeeeHandlers = {
|
||||
'TipeeeStream.Donation': (response) => {
|
||||
tipeeeStreamDonation(response.data);
|
||||
},
|
||||
};
|
||||
|
||||
if (showTipeee) {
|
||||
registerPlatformHandlersToStreamerBot(tipeeeHandlers, '[Tipeeestream]');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// TIPEEESTREAM EVENTS HANDLERS
|
||||
|
||||
async function tipeeeStreamDonation(data) {
|
||||
|
||||
if (tipeeeStreamDonation == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = createRandomString(40);
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['tipeeestream', 'donation'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
var money = formatCurrency(data.amount,data.currency);
|
||||
|
||||
user.innerHTML = `<strong>${data.username}</strong>`;
|
||||
action.innerHTML = ` donated `;
|
||||
value.innerHTML = `<strong>${money}</strong>`;
|
||||
|
||||
if (data.message) message.innerHTML = `: ${data.message}`;
|
||||
|
||||
addEventItem('tipeeestream', clone, classes, userId, messageId);
|
||||
}
|
17
js/modules/twitch/images/logo-twitch.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 800 800">
|
||||
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #a970ff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<polygon class="st0" points="668.5 400 482 602 208 560 224 73 678 91 668.5 400"/>
|
||||
<path class="st1" d="M723.1,53.8v357l-242.4,229.5h-107.7l-134.6,127.5v-127.5H76.9V181.3L211.5,53.8h511.7ZM669.3,104.8H238.4v382.5h121.2v89.3l94.3-89.3h107.7l107.7-102V104.8ZM440.4,350.5h-53.9v-153.1h53.9v153.1ZM588.5,351.1h-53.9v-153h53.9v153Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 698 B |
34
js/modules/twitch/module.css
Normal file
@@ -0,0 +1,34 @@
|
||||
#chat .item.announcement.twitch .message {
|
||||
background: rgba(145,70,255,0.75);
|
||||
}
|
||||
|
||||
#chat .item.twitch .platform .hidden-platform {
|
||||
background: rgba(145,70,255,1);
|
||||
}
|
||||
|
||||
#chat .item.announcement.twitch.green .message {
|
||||
background: linear-gradient(to bottom, rgba(0,219,132,0.75),rgba(87,190,230,0.75));
|
||||
}
|
||||
|
||||
#chat .item.announcement.twitch.purple .message {
|
||||
background: linear-gradient(to bottom, rgba(145,70,255,0.75),rgba(255,117,230,0.75));
|
||||
}
|
||||
|
||||
#chat .item.announcement.twitch.orange .message {
|
||||
background: linear-gradient(to bottom, rgba(255, 179, 26,0.75),rgba(224,224,0,0.75));
|
||||
}
|
||||
|
||||
#chat .item.announcement.twitch.blue .message {
|
||||
background: linear-gradient(to bottom, rgba(0,214,214,0.75),rgba(145,70,255,0.75));
|
||||
}
|
||||
|
||||
#chat .event.twitch .message {
|
||||
background: rgba(145,70,255,0.75);
|
||||
}
|
||||
|
||||
#chat .item img.gigantified {
|
||||
display: block;
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
#statistics .platform#twitch { background: #a970ff; }
|
765
js/modules/twitch/module.js
Normal file
@@ -0,0 +1,765 @@
|
||||
/* ----------------------- */
|
||||
/* TWITCH MODULE VARIABLES */
|
||||
/* ----------------------- */
|
||||
|
||||
const showTwitch = getURLParam("showTwitch", true);
|
||||
|
||||
const showTwitchMessages = getURLParam("showTwitchMessages", true);
|
||||
const showTwitchFollows = getURLParam("showTwitchFollows", true);
|
||||
const showTwitchBits = getURLParam("showTwitchBits", true);
|
||||
const showTwitchAnnouncements = getURLParam("showTwitchAnnouncements", true);
|
||||
const showTwitchSubs = getURLParam("showTwitchSubs", true);
|
||||
const showTwitchGiftedSubs = getURLParam("showTwitchGiftedSubs", true);
|
||||
const showTwitchGiftedSubsUserTrain = getURLParam("showTwitchGiftedSubsUserTrain", true);
|
||||
const showTwitchMassGiftedSubs = getURLParam("showTwitchMassGiftedSubs", true);
|
||||
const showTwitchRewardRedemptions = getURLParam("showTwitchRewardRedemptions", true);
|
||||
const showTwitchRaids = getURLParam("showTwitchRaids", true);
|
||||
const showTwitchSharedChat = getURLParam("showTwitchSharedChat", true);
|
||||
const showTwitchPronouns = getURLParam("showTwitchPronouns", false);
|
||||
const showTwitchViewers = getURLParam("showTwitchViewers", true);
|
||||
|
||||
const twitchAvatars = new Map();
|
||||
const twitchPronouns = new Map();
|
||||
|
||||
// TWITCH EVENTS HANDLERS
|
||||
|
||||
const twitchMessageHandlers = {
|
||||
'Twitch.ChatMessage': (response) => {
|
||||
twitchChatMessage(response.data);
|
||||
},
|
||||
'Twitch.Follow': (response) => {
|
||||
twitchFollowMessage(response.data);
|
||||
},
|
||||
'Twitch.Announcement': (response) => {
|
||||
twitchAnnouncementMessage(response.data);
|
||||
},
|
||||
'Twitch.Cheer': (response) => {
|
||||
twitchBitsMessage(response.data);
|
||||
},
|
||||
'Twitch.AutomaticRewardRedemption': (response) => {
|
||||
twitchChatMessageGiantEmote(response.data);
|
||||
},
|
||||
'Twitch.RewardRedemption': (response) => {
|
||||
twitchRewardRedemption(response.data);
|
||||
},
|
||||
'Twitch.Sub': (response) => {
|
||||
twitchSubMessage(response.data);
|
||||
},
|
||||
'Twitch.ReSub': (response) => {
|
||||
twitchReSubMessage(response.data);
|
||||
},
|
||||
'Twitch.GiftSub': (response) => {
|
||||
twitchGiftMessage(response.data);
|
||||
},
|
||||
'Twitch.GiftBomb': (response) => {
|
||||
twitchGiftBombMessage(response.data);
|
||||
},
|
||||
'Twitch.Raid': (response) => {
|
||||
twitchRaidMessage(response.data);
|
||||
},
|
||||
'Twitch.ChatMessageDeleted': (response) => {
|
||||
twitchChatMessageDeleted(response.data);
|
||||
},
|
||||
'Twitch.UserBanned': (response) => {
|
||||
twitchUserBanned(response.data);
|
||||
},
|
||||
'Twitch.UserTimedOut': (response) => {
|
||||
twitchUserBanned(response.data);
|
||||
},
|
||||
'Twitch.ViewerCountUpdate': (response) => {
|
||||
twitchUpdateStatistics(response.data);
|
||||
},
|
||||
'Twitch.ChatCleared': (response) => {
|
||||
twitchChatClearMessages();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (showTwitch) {
|
||||
|
||||
const twitchStatistics = `
|
||||
<div class="platform" id="twitch" style="display: none;">
|
||||
<img src="js/modules/twitch/images/logo-twitch.svg" alt="">
|
||||
<span class="viewers"><i class="fa-solid fa-user"></i> <span>0</span></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.querySelector('#statistics').insertAdjacentHTML('beforeend', twitchStatistics);
|
||||
|
||||
if (showTwitchViewers == true) { document.querySelector('#twitch').style.display = ''; }
|
||||
|
||||
registerPlatformHandlersToStreamerBot(twitchMessageHandlers, '[Twitch]');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// TWITCH EVENT FUNCTIONS
|
||||
|
||||
async function twitchChatMessage(data) {
|
||||
|
||||
if (showTwitchMessages == false) return;
|
||||
if (ignoreUserList.includes(data.message.username.toLowerCase())) return;
|
||||
if (data.message.message.startsWith("!") && excludeCommands == true) return;
|
||||
|
||||
const template = chatTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.messageId;
|
||||
const userId = data.message.username;
|
||||
|
||||
const {
|
||||
'first-message': firstMessage,
|
||||
'shared-chat': sharedChat,
|
||||
|
||||
header,
|
||||
timestamp,
|
||||
platform,
|
||||
badges,
|
||||
avatar,
|
||||
pronouns: pronoun,
|
||||
user,
|
||||
|
||||
reply,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['twitch', 'chat'];
|
||||
|
||||
const [avatarImage, fullmessage, badgeList] = await Promise.all([
|
||||
getTwitchAvatar(data.message.username),
|
||||
getTwitchEmotes(data),
|
||||
getTwitchBadges(data)
|
||||
]);
|
||||
|
||||
header.remove();
|
||||
|
||||
user.style.color = data.message.color;
|
||||
user.innerHTML = `<strong>${data.message.displayName}</strong>`;
|
||||
message.innerHTML = fullmessage;
|
||||
|
||||
if (data.message.isMe) {
|
||||
message.style.color = data.message.color;
|
||||
}
|
||||
|
||||
if (showAvatar) avatar.innerHTML = `<img src="${avatarImage}">`; else avatar.remove();
|
||||
if (showBadges) badges.innerHTML = badgeList; else badges.remove();
|
||||
|
||||
|
||||
if (data.user.role == 4) { classes.push('streamer'); }
|
||||
|
||||
|
||||
if (data.message.firstMessage) {
|
||||
classes.push('first-chatter');
|
||||
firstMessage.innerHTML('beforeend', ` First-time Chatter`);
|
||||
}
|
||||
else { firstMessage.remove(); }
|
||||
|
||||
|
||||
if (data.message.isReply) {
|
||||
classes.push('reply');
|
||||
reply.insertAdjacentHTML('beforeend', ` <strong>${data.message.reply.userName}:</strong> ${data.message.reply.msgBody}`);
|
||||
}
|
||||
else { reply.remove(); }
|
||||
|
||||
|
||||
if (data.message.isSharedChat) {
|
||||
if (showTwitchSharedChat == true) {
|
||||
classes.push('shared-chat');
|
||||
|
||||
if (!data.sharedChat.primarySource) {
|
||||
sharedChat.querySelector('span.origin').insertAdjacentHTML('beforeend', ` <strong>${data.sharedChat.sourceRoom.name}</strong>`);
|
||||
}
|
||||
}
|
||||
else if (!data.sharedChat.primarySource && showTwitchSharedChat == false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else { sharedChat.remove(); }
|
||||
|
||||
|
||||
|
||||
if (showTwitchPronouns === true) {
|
||||
const userPronouns = await getTwitchUserPronouns(data.message.username);
|
||||
if (userPronouns) {
|
||||
pronoun.innerHTML = userPronouns;
|
||||
}
|
||||
}
|
||||
else { pronoun.remove(); }
|
||||
|
||||
addMessageItem('twitch', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchChatMessageGiantEmote(data) {
|
||||
|
||||
if (showTwitchMessages == false) return;
|
||||
|
||||
const userMessages = chatContainer.querySelectorAll(`.chat.twitch[data-user="${data.user_login}"]`);
|
||||
|
||||
if (userMessages.length === 0) return;
|
||||
|
||||
const firstMessage = userMessages[0];
|
||||
const emoteImages = firstMessage.querySelectorAll(`img[data-emote-id="${data.gigantified_emote.id}"]`);
|
||||
|
||||
if (emoteImages.length === 0) return;
|
||||
|
||||
emoteImages.forEach(img => {
|
||||
img.classList.add("gigantified");
|
||||
if (img.src.endsWith("2.0")) {
|
||||
img.src = img.src.replace("2.0", "3.0");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function twitchFollowMessage(data) {
|
||||
|
||||
if (showTwitchFollows == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.user_name.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['twitch', 'follow'];
|
||||
|
||||
header.remove();
|
||||
message.remove();
|
||||
value.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user_name}</strong>`;
|
||||
|
||||
action.innerHTML = ` followed you`;
|
||||
|
||||
addEventItem('twitch', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchAnnouncementMessage(data) {
|
||||
|
||||
if (showTwitchAnnouncements == false) return;
|
||||
|
||||
const template = chatTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.messageId;
|
||||
const userId = data.user.name.toLowerCase();
|
||||
|
||||
const {
|
||||
'first-message': firstMessage,
|
||||
'shared-chat': sharedChat,
|
||||
|
||||
header,
|
||||
timestamp,
|
||||
platform,
|
||||
badges,
|
||||
avatar,
|
||||
pronouns: pronoun,
|
||||
user,
|
||||
|
||||
reply,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['twitch', 'announcement'];
|
||||
|
||||
classes.push(data.announcementColor.toLowerCase());
|
||||
|
||||
firstMessage.remove();
|
||||
sharedChat.remove();
|
||||
timestamp.remove();
|
||||
//platform.remove();
|
||||
avatar.remove();
|
||||
pronoun.remove();
|
||||
reply.remove();
|
||||
|
||||
const [fullmessage, badgeList] = await Promise.all([
|
||||
getTwitchEmotesOnParts(data),
|
||||
getTwitchAnnouncementBadges(data)
|
||||
]);
|
||||
|
||||
header.innerHTML = `<span><i class="fa-solid fa-bullhorn"></i> Announcement</span>`;
|
||||
|
||||
user.style.color = data.user.color;
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
message.innerHTML = fullmessage;
|
||||
|
||||
if (showBadges) badges.innerHTML = badgeList; else badges.remove();
|
||||
|
||||
addMessageItem('twitch', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchRewardRedemption(data) {
|
||||
|
||||
if (showTwitchRewardRedemptions == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.user_name.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['twitch', 'reward'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user_name}</strong>`;
|
||||
action.innerHTML = ` redeemed `;
|
||||
value.innerHTML = `<strong>${data.reward.title}</strong> (${data.reward.cost})`;
|
||||
|
||||
var userInput = data.user_input ? `- ${data.user_input}` : '';
|
||||
message.innerHTML = `${userInput}`;
|
||||
|
||||
addEventItem('twitch', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchBitsMessage(data) {
|
||||
|
||||
if (showTwitchBits == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.messageId;
|
||||
const userId = data.user.name.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['twitch', 'bits'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
action.innerHTML = ` cheered with `;
|
||||
|
||||
var bits = data.message.bits > 1 ? 'bits' : 'bit';
|
||||
value.innerHTML = `<strong>${data.message.bits} ${bits}</strong>`;
|
||||
|
||||
var fullmessage = data.message.message.replace(/\bCheer\d+\b/g, '').replace(/\s+/g, ' ').trim();
|
||||
message.innerHTML = fullmessage;
|
||||
|
||||
addEventItem('twitch', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchSubMessage(data) {
|
||||
|
||||
if (showTwitchSubs == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.user.name.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['twitch', 'sub'];
|
||||
|
||||
header.remove();
|
||||
message.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
|
||||
action.innerHTML = ` subscribed for `;
|
||||
|
||||
var months = data.duration_months > 1 ? 'months' : 'month';
|
||||
var tier = data.is_prime ? 'Prime' : 'Tier '+Math.floor(data.sub_tier/1000);
|
||||
|
||||
value.innerHTML = `<strong>${data.duration_months} ${months} (${tier})</strong>`;
|
||||
|
||||
addEventItem('twitch', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchReSubMessage(data) {
|
||||
|
||||
if (showTwitchSubs == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.user.name.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['twitch', 'resub'];
|
||||
|
||||
const [fullmessage] = await Promise.all([
|
||||
getTwitchEmotesOnParts(data)
|
||||
]);
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
|
||||
action.innerHTML = ` subscribed for `;
|
||||
|
||||
var months = data.cumulativeMonths > 1 ? 'months' : 'month';
|
||||
var tier = data.isPrime ? 'Prime' : 'Tier '+Math.floor(data.subTier/1000);
|
||||
|
||||
value.innerHTML = `<strong>${data.cumulativeMonths} ${months} (${tier})</strong>`;
|
||||
|
||||
message.innerHTML = fullmessage;
|
||||
|
||||
addEventItem('twitch', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchGiftMessage(data) {
|
||||
|
||||
const isSub = showTwitchSubs === false;
|
||||
const isGift = showTwitchGiftedSubs === false;
|
||||
const isGiftTrain = showTwitchGiftedSubsUserTrain === false;
|
||||
|
||||
if (
|
||||
(!data.fromCommunitySubGift && (isSub || isGift)) ||
|
||||
(data.fromCommunitySubGift && (isSub || isGiftTrain))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.user.name.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['twitch', 'giftsub'];
|
||||
|
||||
header.remove();
|
||||
message.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
|
||||
var months = data.durationMonths > 1 ? 'months' : 'month';
|
||||
action.innerHTML = ` gifted <strong>${data.durationMonths} ${months}</strong> subscription <strong>(Tier ${Math.floor(data.subTier/1000)})</strong> to `;
|
||||
|
||||
value.innerHTML = `<strong>${data.recipient.name}</strong>`;
|
||||
|
||||
addEventItem('twitch', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchGiftBombMessage(data) {
|
||||
|
||||
if (showTwitchSubs == false || showTwitchMassGiftedSubs == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.user.name.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['twitch', 'giftbomb'];
|
||||
|
||||
header.remove();
|
||||
value.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
|
||||
var subs = data.total > 1 ? 'subs' : 'sub';
|
||||
action.innerHTML = ` gifted <strong>${data.total} Tier ${Math.floor(data.sub_tier/1000)} ${subs}</strong> to the Community`;
|
||||
|
||||
message.innerHTML = `They've gifted a total of <strong>${data.cumulative_total} subs</strong>`;
|
||||
|
||||
addEventItem('twitch', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchRaidMessage(data) {
|
||||
|
||||
if (showTwitchRaids == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = createRandomString(40);
|
||||
const userId = data.from_broadcaster_user_name.toLowerCase();
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['twitch', 'raid'];
|
||||
|
||||
header.remove();
|
||||
message.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.from_broadcaster_user_name}</strong>`;
|
||||
|
||||
var viewers = data.viewers > 1 ? 'viewers' : 'viewer';
|
||||
action.innerHTML = ` raided the channel with `;
|
||||
value.innerHTML = `<strong>${data.viewers} ${viewers}</strong>`;
|
||||
|
||||
addEventItem('twitch', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchChatMessageDeleted(data) {
|
||||
document.getElementById(data.messageId)?.remove();
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchUserBanned(data) {
|
||||
chatContainer.querySelectorAll(`[data-user="${data.user_login}"]`).forEach(element => {
|
||||
element.remove();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchChatClearMessages() {
|
||||
chatContainer.querySelectorAll(`.item.twitch`).forEach(element => {
|
||||
element.remove();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function twitchUpdateStatistics(data) {
|
||||
if (showPlatformStatistics == false || showTwitchViewers == false) return;
|
||||
|
||||
const viewers = DOMPurify.sanitize(data.viewerCount);
|
||||
document.querySelector('#statistics #twitch .viewers span').textContent = formatNumber(viewers);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// TWITCH UTILITY FUNCTIONS
|
||||
|
||||
async function getTwitchAvatar(user) {
|
||||
if (twitchAvatars.has(user)) {
|
||||
console.debug(`Twitch avatar found for ${user}!`);
|
||||
return twitchAvatars.get(user);
|
||||
}
|
||||
|
||||
console.debug(`Twitch avatar not found for ${user}! Getting it from DECAPI!`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://decapi.me/twitch/avatar/${user}`);
|
||||
let avatar = await response.text();
|
||||
|
||||
if (!avatar) {
|
||||
avatar = 'https://static-cdn.jtvnw.net/user-default-pictures-uv/cdd517fe-def4-11e9-948e-784f43822e80-profile_image-300x300.png';
|
||||
}
|
||||
|
||||
twitchAvatars.set(user, avatar);
|
||||
return avatar;
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`Failed to fetch avatar for ${user}:`, err);
|
||||
return 'https://static-cdn.jtvnw.net/user-default-pictures-uv/cdd517fe-def4-11e9-948e-784f43822e80-profile_image-300x300.png';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function getTwitchEmotes(data) {
|
||||
const message = data.message.message;
|
||||
const emotes = data.emotes.sort((a, b) => b.startIndex - a.startIndex);
|
||||
|
||||
const words = message.split(" ").map(word => {
|
||||
const emote = emotes.find(e => e.name === word);
|
||||
return emote
|
||||
? `<img src="${emote.imageUrl}" data-emote-id="${emote.id}" alt="${emote.name}" class="emote">`
|
||||
: word;
|
||||
});
|
||||
|
||||
return words.join(" ");
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function getTwitchBadges(data) {
|
||||
const badges = data.message.badges;
|
||||
return badges
|
||||
.map(badge => `<img src="${badge.imageUrl}" class="badge">`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function getTwitchEmotesOnParts(data) {
|
||||
let messageText = data.text;
|
||||
|
||||
for (const part of data.parts) {
|
||||
if (part.type === 'emote') {
|
||||
const emoteName = part.text;
|
||||
const emoteUrl = part.imageUrl;
|
||||
const emoteHTML = `<img src="${emoteUrl}" class="emote" alt="${emoteName}">`;
|
||||
|
||||
const escaped = emoteName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
let pattern;
|
||||
|
||||
if (/^\w+$/.test(emoteName)) {
|
||||
pattern = `\\b${escaped}\\b`;
|
||||
} else {
|
||||
pattern = `(?<=^|[^\\w])${escaped}(?=$|[^\\w])`;
|
||||
}
|
||||
|
||||
const regex = new RegExp(pattern, 'g');
|
||||
messageText = messageText.replace(regex, emoteHTML);
|
||||
}
|
||||
}
|
||||
|
||||
return messageText;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function getTwitchAnnouncementBadges(data) {
|
||||
const badges = data.user.badges;
|
||||
return badges
|
||||
.map(badge => `<img src="${badge.imageUrl}" class="badge">`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
|
||||
async function getTwitchUserPronouns(username) {
|
||||
if (twitchPronouns.has(username)) {
|
||||
console.debug(`Pronouns found for ${username}. Getting it from Map...`);
|
||||
return twitchPronouns.get(username);
|
||||
}
|
||||
|
||||
console.debug(`Pronouns not found for ${username} in the Map! Retrieving...`);
|
||||
|
||||
try {
|
||||
const response = await streamerBotClient.getUserPronouns('twitch', username);
|
||||
|
||||
const pronoun = response?.pronoun?.userFound
|
||||
? `<em>${response.pronoun.pronounSubject}/${response.pronoun.pronounObject}</em>`
|
||||
: '';
|
||||
|
||||
twitchPronouns.set(username, pronoun);
|
||||
return pronoun;
|
||||
}
|
||||
|
||||
catch (err) {
|
||||
console.error(`Couldn't retrieve pronouns for ${username}:`, err);
|
||||
return '';
|
||||
}
|
||||
}
|
17
js/modules/youtube/images/logo-youtube.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 800 800">
|
||||
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: red;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="st0" x="283" y="283" width="284" height="272"/>
|
||||
<path class="st1" d="M744.2,410.3s0,112.7-14.4,166.8c-7.9,29.8-31.2,52.3-60.9,60.3-53.7,14.5-268.9,14.5-268.9,14.5,0,0-215.3,0-268.9-14.5-29.6-8-52.9-30.5-60.9-60.3-14.4-54-14.4-166.8-14.4-166.8,0,0,0-112.7,14.4-166.8,7.9-29.8,31.2-53.3,60.9-61.3,53.7-14.5,268.9-14.5,268.9-14.5,0,0,215.3,0,268.9,14.5,29.6,8,52.9,31.4,60.9,61.3,14.4,54,14.4,166.8,14.4,166.8ZM509.5,410.3l-179.9-102.4v204.7l179.9-102.3Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 833 B |
57
js/modules/youtube/module.css
Normal file
@@ -0,0 +1,57 @@
|
||||
#chat .event.youtube .message {
|
||||
background: rgba(255,0,0,0.75);
|
||||
}
|
||||
|
||||
#chat .event.youtube.sticker .header {
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
}
|
||||
#chat .event.youtube.sticker .header img {
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
#chat .item.kick .platform .hidden-platform {
|
||||
background: rgba(255,0,0,1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#chat .item.youtube span.badge {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
vertical-align: top;
|
||||
color: #FFF;
|
||||
background: rgba(255,255,255);
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
#chat .item.youtube span.badge.owner {
|
||||
background: #FF0000;
|
||||
}
|
||||
|
||||
#chat .item.youtube span.mod {
|
||||
background: #5e84f1;
|
||||
}
|
||||
|
||||
#chat .item.youtube span.member {
|
||||
background: #2ba640;
|
||||
}
|
||||
#chat .item.youtube span.verified {
|
||||
background: #aaa;
|
||||
}
|
||||
|
||||
#chat .item.youtube.streamer .user {
|
||||
background: #ffd600;
|
||||
color: #121212 !important;
|
||||
text-shadow: none;
|
||||
box-shadow: 2px 2px 5px rgba(0,0,0,0.25);
|
||||
padding: 0 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#statistics .platform#youtube { background: #FF0000; }
|
509
js/modules/youtube/module.js
Normal file
@@ -0,0 +1,509 @@
|
||||
/* ------------------------ */
|
||||
/* YOUTUBE MODULE VARIABLES */
|
||||
/* ------------------------ */
|
||||
|
||||
const showYoutube = getURLParam("showYoutube", true);
|
||||
|
||||
const showYouTubeMessages = getURLParam("showYouTubeMessages", true);
|
||||
const showYouTubeSuperChats = getURLParam("showYouTubeSuperChats", true);
|
||||
const showYouTubeSuperStickers = getURLParam("showYouTubeSuperStickers", true);
|
||||
const showYouTubeSuperStickerGif = getURLParam("showYouTubeSuperStickerGif", true);
|
||||
const showYouTubeMemberships = getURLParam("showYouTubeMemberships", true);
|
||||
const showYouTubeGiftMemberships = getURLParam("showYouTubeGiftMemberships", true);
|
||||
const showYouTubeMembershipsTrain = getURLParam("showYouTubeMembershipsTrain", true);
|
||||
const showYouTubeStatistics = getURLParam("showYouTubeStatistics", true);
|
||||
|
||||
let youTubeCustomEmotes = [];
|
||||
let youTubeBTTVEmotes = [];
|
||||
|
||||
userColors.set('youtube', new Map());
|
||||
|
||||
|
||||
// YOUTUBE EVENTS HANDLERS
|
||||
|
||||
const youtubeMessageHandlers = {
|
||||
'YouTube.Message': (response) => {
|
||||
youTubeChatMessage(response.data);
|
||||
},
|
||||
'YouTube.UserBanned': (response) => {
|
||||
youTubeUserBanned(response.data);
|
||||
},
|
||||
'YouTube.SuperChat': (response) => {
|
||||
youTubeSuperChatMessage(response.data);
|
||||
},
|
||||
'YouTube.SuperSticker': (response) => {
|
||||
youTubeSuperStickerMessage(response.data);
|
||||
},
|
||||
'YouTube.NewSponsor': (response) => {
|
||||
youTubeNewSponsorMessage(response.data);
|
||||
},
|
||||
'YouTube.MemberMileStone': (response) => {
|
||||
youTubeNewSponsorMessage(response.data);
|
||||
},
|
||||
'YouTube.MembershipGift': (response) => {
|
||||
youTubeGiftBombMessage(response.data);
|
||||
},
|
||||
'YouTube.GiftMembershipReceived': (response) => {
|
||||
youTubeGiftBombReceivedMessage(response.data);
|
||||
},
|
||||
'YouTube.StatisticsUpdated': (response) => {
|
||||
youTubeUpdateStatistics(response.data);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (showYoutube) {
|
||||
|
||||
const youtubeStatistics = `
|
||||
<div class="platform" id="youtube" style="display: none;">
|
||||
<img src="js/modules/youtube/images/logo-youtube.svg" alt="">
|
||||
<span class="viewers"><i class="fa-solid fa-user"></i> <span>0</span></span>
|
||||
<span class="likes"><i class="fa-solid fa-heart"></i> <span>0</span></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.querySelector('#statistics').insertAdjacentHTML('beforeend', youtubeStatistics);
|
||||
|
||||
if (showYouTubeStatistics == true) { document.querySelector('#youtube').style.display = ''; }
|
||||
|
||||
registerPlatformHandlersToStreamerBot(youtubeMessageHandlers, '[YouTube]');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// YOUTUBE EVENT FUNCTIONS
|
||||
|
||||
async function youTubeChatMessage(data) {
|
||||
|
||||
if (showYouTubeMessages == false) return;
|
||||
if (ignoreUserList.includes(data.user.name.toLowerCase())) return;
|
||||
if (data.message.startsWith("!") && excludeCommands == true) return;
|
||||
|
||||
const template = chatTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.eventId;
|
||||
const userId = data.user.id;
|
||||
|
||||
const {
|
||||
'first-message': firstMessage,
|
||||
'shared-chat': sharedChat,
|
||||
|
||||
header,
|
||||
timestamp,
|
||||
platform,
|
||||
badges,
|
||||
avatar,
|
||||
pronouns: pronoun,
|
||||
user,
|
||||
|
||||
reply,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['youtube', 'chat'];
|
||||
|
||||
const [fullmessage, badgeList] = await Promise.all([
|
||||
getYouTubeEmotes(data),
|
||||
getYouTubeBadges(data)
|
||||
]);
|
||||
|
||||
header.remove();
|
||||
firstMessage.remove();
|
||||
sharedChat.remove();
|
||||
reply.remove();
|
||||
pronoun.remove();
|
||||
|
||||
var color = await createRandomColor('youtube', data.user.name);
|
||||
|
||||
user.style.color = color;
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
message.innerHTML = fullmessage;
|
||||
|
||||
if (showAvatar) avatar.innerHTML = `<img src="${data.user.profileImageUrl}">`; else avatar.remove();
|
||||
if (showBadges) badges.innerHTML = badgeList; else badges.remove();
|
||||
|
||||
if (data.user.isOwner) { classes.push('streamer'); }
|
||||
|
||||
addMessageItem('youtube', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function youTubeSuperChatMessage(data) {
|
||||
|
||||
if (showYouTubeSuperChats == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.eventId;
|
||||
const userId = data.user.id;
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['youtube', 'superchat'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
action.innerHTML = ` superchatted `;
|
||||
value.innerHTML = `<strong>${data.amount}</strong>`;
|
||||
|
||||
var fullmessage = await getYouTubeEmotes(data);
|
||||
message.innerHTML = fullmessage;
|
||||
|
||||
addEventItem('youtube', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function youTubeSuperStickerMessage(data) {
|
||||
|
||||
if (showYouTubeMemberships == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.eventId;
|
||||
const userId = data.user.id;
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['youtube', 'sticker'];
|
||||
|
||||
if (showYouTubeSuperStickerGif == true) {
|
||||
youtubeStickerUrl = await getYouTubeStickerImage(data);
|
||||
header.innerHTML = `<img src="${youtubeStickerUrl}" class="sticker">`;
|
||||
}
|
||||
else {
|
||||
header.remove();
|
||||
}
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
action.innerHTML = ` sent a supersticker `;
|
||||
|
||||
value.innerHTML = `<strong>(${data.amount})</strong>`;
|
||||
|
||||
message.remove();
|
||||
|
||||
addEventItem('youtube', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function youTubeNewSponsorMessage(data) {
|
||||
|
||||
if (showYouTubeMemberships == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.eventId;
|
||||
const userId = data.user.id;
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['youtube', 'sponsor'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
action.innerHTML = ` became a member `;
|
||||
|
||||
var months = data.months > 1 ? 'months' : 'month';
|
||||
value.innerHTML = `<strong>${data.months || 1} ${months}</strong>`;
|
||||
|
||||
var fullmessage = await getYouTubeEmotes(data);
|
||||
message.innerHTML = fullmessage;
|
||||
|
||||
addEventItem('youtube', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function youTubeGiftBombMessage(data) {
|
||||
|
||||
if (showYouTubeMemberships == false || showYouTubeGiftMemberships == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.eventId;
|
||||
const userId = data.user.id;
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['youtube', 'giftbomb'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
action.innerHTML = ` gifted `;
|
||||
|
||||
var count = data.count > 1 ? 'memberships' : 'membership';
|
||||
value.innerHTML = `<strong>${data.count} ${count} (Tier ${data.tier})</strong> to the channel`;
|
||||
|
||||
message.remove();
|
||||
|
||||
addEventItem('youtube', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function youTubeGiftBombReceivedMessage(data) {
|
||||
|
||||
if (showYouTubeMemberships == false || showYouTubeGiftMemberships == false || showYouTubeMembershipsTrain == false) return;
|
||||
|
||||
const template = eventTemplate;
|
||||
const clone = template.content.cloneNode(true);
|
||||
const messageId = data.eventId;
|
||||
const userId = data.user.id;
|
||||
|
||||
const {
|
||||
header,
|
||||
platform,
|
||||
user,
|
||||
action,
|
||||
value,
|
||||
'actual-message': message
|
||||
} = Object.fromEntries(
|
||||
[...clone.querySelectorAll('[class]')]
|
||||
.map(el => [el.className, el])
|
||||
);
|
||||
|
||||
const classes = ['youtube', 'giftbomb'];
|
||||
|
||||
header.remove();
|
||||
|
||||
|
||||
user.innerHTML = `<strong>${data.user.name}</strong>`;
|
||||
action.innerHTML = ` gifted a membership <strong>(Tier ${data.tier})</strong> to `;
|
||||
value.innerHTML = `<strong>${data.gifter.name}</strong>`;
|
||||
|
||||
message.remove();
|
||||
|
||||
addEventItem('youtube', clone, classes, userId, messageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function youTubeUserBanned(data) {
|
||||
chatContainer.querySelectorAll(`[data-user="${data.bannedUser.id}"]:not(.event)`).forEach(element => {
|
||||
element.remove();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function youTubeUpdateStatistics(data) {
|
||||
|
||||
if (showPlatformStatistics == false || showYouTubeStatistics == false) return;
|
||||
|
||||
const viewers = DOMPurify.sanitize(data.concurrentViewers);
|
||||
const likes = DOMPurify.sanitize(data.likeCount);
|
||||
document.querySelector('#statistics #youtube .viewers span').textContent = formatNumber(viewers);
|
||||
document.querySelector('#statistics #youtube .likes span').textContent = formatNumber(likes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------------------------
|
||||
// YOUTUBE UTILITY FUNCTIONS
|
||||
|
||||
|
||||
|
||||
async function getYouTubeEmotes(data) {
|
||||
let message = data.message;
|
||||
const channelId = data.broadcast?.channelId;
|
||||
if (!channelId) return message;
|
||||
|
||||
// Load BTTV Emotes if not already loaded
|
||||
if (youTubeBTTVEmotes.length === 0) {
|
||||
try {
|
||||
const res = await fetch(`https://api.betterttv.net/3/cached/users/youtube/${channelId}`);
|
||||
const emoteData = await res.json();
|
||||
console.debug('Getting YouTube BTTV Channel Emotes', `https://api.betterttv.net/3/cached/users/youtube/${channelId}`, emoteData);
|
||||
youTubeBTTVEmotes = [
|
||||
...(emoteData.sharedEmotes || []),
|
||||
...(emoteData.channelEmotes || [])
|
||||
];
|
||||
} catch (err) {
|
||||
console.warn("[YouTube] Failed to load BTTV emotes:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Create an Emote Map
|
||||
const emoteMap = new Map();
|
||||
|
||||
// BTTV emotes
|
||||
for (const emote of youTubeBTTVEmotes) {
|
||||
const imageUrl = `https://cdn.betterttv.net/emote/${emote.id}/1x`;
|
||||
const emoteElement = `<img src="${imageUrl}" class="emote" alt="${emote.code}">`;
|
||||
emoteMap.set(emote.code, { html: emoteElement, raw: emote.code });
|
||||
}
|
||||
|
||||
// YouTube emotes (ex: :hand-pink-waving:)
|
||||
if (data.emotes) {
|
||||
for (const emote of data.emotes) {
|
||||
const emoteElement = `<img src="${emote.imageUrl}" class="emote" alt="${emote.name}">`;
|
||||
emoteMap.set(emote.name, { html: emoteElement, raw: emote.name });
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Member Emotes
|
||||
if (data.user.isSponsor === true || data.user.isOwner === true) {
|
||||
for (const [name, url] of Object.entries(youTubeCustomEmotes)) {
|
||||
const emoteElement = `<img src="${url}" class="emote" alt="${name}">`;
|
||||
emoteMap.set(`:${name}:`, { html: emoteElement, raw: `:${name}:` });
|
||||
}
|
||||
}
|
||||
|
||||
// DOMParser just to replace the text nodes
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(`<div>${message}</div>`, 'text/html');
|
||||
const container = doc.body.firstChild;
|
||||
|
||||
function escapeRegex(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function replaceEmotesInText(text) {
|
||||
// Sort them DESC to avoid conflicts with similar names
|
||||
const sorted = Array.from(emoteMap.values()).sort((a, b) => b.raw.length - a.raw.length);
|
||||
|
||||
for (const { raw, html } of sorted) {
|
||||
const escaped = escapeRegex(raw);
|
||||
|
||||
// Emotes with colons: :emote: → allow colons
|
||||
const isDelimited = raw.startsWith(':') && raw.endsWith(':');
|
||||
const regex = isDelimited
|
||||
? new RegExp(escaped, 'g')
|
||||
: new RegExp(`(?<!\\S)${escaped}(?!\\S)`, 'g');
|
||||
|
||||
text = text.replace(regex, html);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function walk(node) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const replaced = replaceEmotesInText(node.nodeValue);
|
||||
if (replaced !== node.nodeValue) {
|
||||
const span = doc.createElement('span');
|
||||
span.innerHTML = replaced;
|
||||
node.replaceWith(...span.childNodes);
|
||||
}
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
for (const child of Array.from(node.childNodes)) {
|
||||
walk(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(container);
|
||||
|
||||
return container.innerHTML;
|
||||
}
|
||||
|
||||
|
||||
// ChatGPT created this. :)
|
||||
async function getYouTubeStickerImage(data) {
|
||||
const stack = [data];
|
||||
|
||||
while (stack.length) {
|
||||
const current = stack.pop();
|
||||
|
||||
if (current && typeof current === 'object') {
|
||||
if ('imageUrl' in current && typeof current.imageUrl === 'string') {
|
||||
return current.imageUrl;
|
||||
}
|
||||
|
||||
for (const key in current) {
|
||||
if (Object.hasOwn(current, key)) {
|
||||
stack.push(current[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getYouTubeBadges(data) {
|
||||
const {
|
||||
user: {
|
||||
isVerified,
|
||||
isSponsor,
|
||||
isModerator,
|
||||
isOwner,
|
||||
}
|
||||
} = data;
|
||||
|
||||
let badgesHTML = [
|
||||
isVerified && '<span class="badge verified"><i class="fa-solid fa-check"></i></span>',
|
||||
isSponsor && '<span class="badge member"><i class="fa-solid fa-star"></i></span>',
|
||||
isModerator && '<span class="badge mod"><i class="fa-solid fa-wrench"></i></span>',
|
||||
isOwner && '<span class="badge owner"><i class="fa-solid fa-video"></i></span>',
|
||||
].filter(Boolean).join('');
|
||||
|
||||
return badgesHTML;
|
||||
}
|
67
js/sb.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/* ----------------------- */
|
||||
/* STREAMER.BOT CONNECTION */
|
||||
/* ----------------------- */
|
||||
|
||||
const streamerBotServerAddress = getURLParam("streamerBotServerAddress", "127.0.0.1");
|
||||
const streamerBotServerPort = getURLParam("streamerBotServerPort", "8080");
|
||||
|
||||
const showSpeakerbot = getURLParam("showSpeakerbot", true);
|
||||
const speakerBotServerAddress = getURLParam("speakerBotServerAddress", "127.0.0.1");
|
||||
const speakerBotServerPort = getURLParam("speakerBotServerPort", "7580");
|
||||
const speakerBotChatRead = getURLParam("speakerBotChatRead", false);
|
||||
const speakerBotEventRead = getURLParam("speakerBotEventRead", false);
|
||||
const speakerBotVoiceAlias = getURLParam("speakerBotVoiceAlias", "Maria");
|
||||
const speakerBotChatTemplate = getURLParam("speakerBotChatTemplate", "{user} said {message}");
|
||||
|
||||
if (showSpeakerbot == true ) {
|
||||
|
||||
const speakerBotClient = new SpeakerBotClient({
|
||||
host: speakerBotServerAddress,
|
||||
port: speakerBotServerPort,
|
||||
voiceAlias: speakerBotVoiceAlias,
|
||||
onConnect: (data) => {
|
||||
|
||||
notifySuccess({
|
||||
title: 'Connected to Speaker.bot',
|
||||
text: ``
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const streamerBotClient = new StreamerbotClient({
|
||||
host: streamerBotServerAddress,
|
||||
port: streamerBotServerPort,
|
||||
|
||||
onConnect: (data) => {
|
||||
console.debug(`[ChatRD][Overlay] Connected to Streamer.bot successfully!`);
|
||||
|
||||
notifySuccess({
|
||||
title: 'Connected to Streamer.bot',
|
||||
text: ``
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function getURLParam(param, defaultValue) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const value = urlParams.get(param);
|
||||
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
if (value === null) return defaultValue;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function registerPlatformHandlersToStreamerBot(handlers, logPrefix = '') {
|
||||
for (const [event, handler] of Object.entries(handlers)) {
|
||||
streamerBotClient.on(event, (...args) => {
|
||||
if (logPrefix) {
|
||||
console.debug(`${logPrefix} ${event}`, args[0]);
|
||||
}
|
||||
handler(...args);
|
||||
});
|
||||
}
|
||||
}
|
605
js/settings.js
@@ -1,165 +1,88 @@
|
||||
let streamerBotClient;
|
||||
let streamerBotConnected = false;
|
||||
let kickWebSocket = null;
|
||||
let tikfinityWebSocket = null;
|
||||
let speakerBotClient = null;
|
||||
|
||||
async function saveSettingsToLocalStorage() {
|
||||
/* -------------------------
|
||||
Salvar configurações no localStorage
|
||||
-------------------------- */
|
||||
function saveSettingsToLocalStorage() {
|
||||
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
|
||||
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
|
||||
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
|
||||
const colorfields = document.querySelectorAll("input[type=color]:not(.avoid)");
|
||||
const selects = document.querySelectorAll("select:not(.avoid)");
|
||||
|
||||
const hiddenField = document.querySelector("textarea[name=youTubeCustomEmotes]:not(.avoid)");
|
||||
|
||||
const ranges = document.querySelectorAll("input[type=range]:not(.avoid)");
|
||||
|
||||
const settings = {};
|
||||
|
||||
checkboxes.forEach((checkbox) => {
|
||||
settings[checkbox.name] = checkbox.checked;
|
||||
});
|
||||
ranges.forEach((range) => {
|
||||
settings[range.name] = range.value;
|
||||
});
|
||||
textfields.forEach((textfield) => {
|
||||
settings[textfield.name] = textfield.value;
|
||||
});
|
||||
numberfields.forEach((numberfield) => {
|
||||
settings[numberfield.name] = numberfield.value;
|
||||
});
|
||||
colorfields.forEach((colorfield) => {
|
||||
settings[colorfield.name] = colorfield.value;
|
||||
});
|
||||
selects.forEach((select) => {
|
||||
settings[select.name] = select.value;
|
||||
});
|
||||
checkboxes.forEach(cb => settings[cb.name] = cb.checked);
|
||||
ranges.forEach(r => settings[r.name] = r.value);
|
||||
textfields.forEach(tf => settings[tf.name] = tf.value);
|
||||
numberfields.forEach(nf => settings[nf.name] = nf.value);
|
||||
colorfields.forEach(cf => settings[cf.name] = cf.value);
|
||||
selects.forEach(s => settings[s.name] = s.value);
|
||||
|
||||
localStorage.setItem("chatWidgetSettings", JSON.stringify(settings));
|
||||
localStorage.setItem("chatrdWidgetSettings", JSON.stringify(settings));
|
||||
|
||||
if (streamerBotConnected == true) {
|
||||
streamerBotClient.doAction(
|
||||
{ name : "YouTube Custom Emotes" },
|
||||
{
|
||||
"chatrdytcustomemotes": JSON.stringify(hiddenField.value.trim()),
|
||||
// Salva emotes no Streamer.bot
|
||||
try {
|
||||
const youtubeMemberEmotes = document.querySelector("textarea[name=youTubeCustomEmotes]:not(.avoid)");
|
||||
youtubeSaveMemberEmotes(JSON.parse(youtubeMemberEmotes.value));
|
||||
}
|
||||
).then( (setglobals) => {
|
||||
console.debug('Saving YouTube Emotes from Streamer.Bot', setglobals);
|
||||
});
|
||||
catch (err) {
|
||||
console.error("[ChatRD] Emotes JSON inválido", err);
|
||||
}
|
||||
|
||||
generateUrl();
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------
|
||||
Carregar configurações do localStorage
|
||||
-------------------------- */
|
||||
async function loadSettingsFromLocalStorage() {
|
||||
const saved = localStorage.getItem("chatWidgetSettings");
|
||||
const saved = localStorage.getItem("chatrdWidgetSettings");
|
||||
if (!saved) return;
|
||||
|
||||
const settings = JSON.parse(saved);
|
||||
console.log(settings);
|
||||
|
||||
Object.keys(settings).forEach((key) => {
|
||||
Object.keys(settings).forEach(key => {
|
||||
const input = document.querySelector(`[name="${key}"]`);
|
||||
if (input) {
|
||||
if (input.type === "checkbox") {
|
||||
input.checked = settings[key];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
input.value = settings[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
document.querySelector('#font-value').textContent = Math.floor(document.querySelector('#font-slider').value * 100) + '%';
|
||||
document.querySelector('#bg-opacity-value').textContent = Math.floor(document.querySelector('#bg-opacity-slider').value * 100) + '%';
|
||||
|
||||
|
||||
var streamerBotServerAddress = document.querySelector('input[type=text][name=streamerBotServerAddress]').value;
|
||||
var streamerBotServerPort = document.querySelector('input[type=text][name=streamerBotServerPort]').value;
|
||||
|
||||
streamerBotClient = new StreamerbotClient({
|
||||
host: streamerBotServerAddress,
|
||||
port: streamerBotServerPort,
|
||||
onConnect: (data) => {
|
||||
streamerBotConnected = true;
|
||||
|
||||
document.querySelector('#memberemotesbstatus').classList.remove('offline');
|
||||
document.querySelector('#memberemotesbstatus').classList.add('online');
|
||||
document.querySelector('#memberemotesbstatus span').textContent = 'Streamer.Bot is Online!';
|
||||
|
||||
streamerBotClient.getGlobals().then( (getglobals) => {
|
||||
const settings = JSON.parse(getglobals.variables.chatrdytcustomemotes.value);
|
||||
console.debug('Getting YouTube Emotes from Streamer.Bot', settings);
|
||||
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
|
||||
textarea.value = settings;
|
||||
|
||||
youtubeLoadMemberEmotes().then(settings => {
|
||||
if (settings) {
|
||||
const youtubeMemberEmotes = document.querySelector("textarea[name=youTubeCustomEmotes]:not(.avoid)");
|
||||
console.log('[ChatRD][Settings] YouTube Member Emotes Loaded', settings);
|
||||
youtubeMemberEmotes.value = JSON.stringify(settings);
|
||||
populateEmoteList();
|
||||
});
|
||||
|
||||
},
|
||||
onDisconnect: () => {
|
||||
console.error('Streamer.bot Disconnected!');
|
||||
|
||||
streamerBotConnected = false;
|
||||
|
||||
document.querySelector('#memberemotesbstatus').classList.remove('online');
|
||||
document.querySelector('#memberemotesbstatus').classList.add('offline');
|
||||
document.querySelector('#memberemotesbstatus span').textContent = 'Streamer.Bot is Offline!';
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function pushChangeEvents() {
|
||||
/* -------------------------
|
||||
Configurar eventos para salvar mudanças
|
||||
-------------------------- */
|
||||
function pushChangeEvents() {
|
||||
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
|
||||
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
|
||||
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
|
||||
const colorfields = document.querySelectorAll("input[type=color]:not(.avoid)");
|
||||
const selects = document.querySelectorAll("select:not(.avoid)");
|
||||
|
||||
const ranges = document.querySelectorAll("input[type=range]:not(.avoid)");
|
||||
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.addEventListener('change', () => {
|
||||
generateUrl();
|
||||
saveSettingsToLocalStorage();
|
||||
});
|
||||
});
|
||||
textfields.forEach((textfield) => {
|
||||
textfield.addEventListener('input', () => {
|
||||
generateUrl();
|
||||
saveSettingsToLocalStorage();
|
||||
});
|
||||
});
|
||||
numberfields.forEach((numberfield) => {
|
||||
numberfield.addEventListener('input', () => {
|
||||
generateUrl();
|
||||
saveSettingsToLocalStorage();
|
||||
});
|
||||
});
|
||||
colorfields.forEach((colorfield) => {
|
||||
colorfield.addEventListener('change', () => {
|
||||
generateUrl();
|
||||
saveSettingsToLocalStorage();
|
||||
});
|
||||
});
|
||||
selects.forEach((select) => {
|
||||
select.addEventListener('change', () => {
|
||||
generateUrl();
|
||||
saveSettingsToLocalStorage();
|
||||
});
|
||||
});
|
||||
textfields.forEach((textfield) => {
|
||||
textfield.addEventListener('input', () => {
|
||||
generateUrl();
|
||||
saveSettingsToLocalStorage();
|
||||
});
|
||||
});
|
||||
|
||||
ranges.forEach((range) => {
|
||||
range.addEventListener('change', () => {
|
||||
generateUrl();
|
||||
saveSettingsToLocalStorage();
|
||||
});
|
||||
[...checkboxes, ...textfields, ...numberfields, ...colorfields, ...selects, ...ranges].forEach(el => {
|
||||
el.addEventListener('change', saveSettingsToLocalStorage);
|
||||
el.addEventListener('input', saveSettingsToLocalStorage);
|
||||
});
|
||||
|
||||
document.querySelector('#font-slider').addEventListener('input', function () {
|
||||
@@ -167,108 +90,211 @@ async function pushChangeEvents() {
|
||||
});
|
||||
|
||||
document.querySelector('#bg-opacity-slider').addEventListener('input', function () {
|
||||
document.querySelector('#bg-opacity-value').textContent = this.value;
|
||||
document.querySelector('#bg-opacity-value').textContent = Math.floor(this.value * 100) + '%';
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
Gerar URL de preview
|
||||
-------------------------- */
|
||||
function generateUrl() {
|
||||
const outputField = document.getElementById("outputUrl");
|
||||
outputField.value = '';
|
||||
|
||||
async function generateUrl() {
|
||||
document.getElementById("outputUrl").value = '';
|
||||
|
||||
|
||||
var baseUrl = 'https://vortisrd.github.io/chatrd/chat.html';
|
||||
const baseUrlObj = new URL(window.location.href);
|
||||
baseUrlObj.pathname = baseUrlObj.pathname.replace(/index\.html$/, "chat.html");
|
||||
const baseUrl = baseUrlObj.toString();
|
||||
|
||||
const checkboxes = document.querySelectorAll("input[type=checkbox]:not(.avoid)");
|
||||
const textfields = document.querySelectorAll("input[type=text]:not(.avoid)");
|
||||
const numberfields = document.querySelectorAll("input[type=number]:not(.avoid)");
|
||||
const colorfields = document.querySelectorAll("input[type=color]:not(.avoid)");
|
||||
const selects = document.querySelectorAll("select:not(.avoid)");
|
||||
|
||||
const ranges = document.querySelectorAll("input[type=range]:not(.avoid)");
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
selects.forEach((select) => {
|
||||
params.set(select.name, select.value);
|
||||
});
|
||||
ranges.forEach((range) => {
|
||||
params.set(range.name, range.value);
|
||||
});
|
||||
checkboxes.forEach((checkbox) => {
|
||||
params.set(checkbox.name, checkbox.checked);
|
||||
});
|
||||
colorfields.forEach((colorfield) => {
|
||||
params.set(colorfield.name, colorfield.value);
|
||||
});
|
||||
textfields.forEach((textfield) => {
|
||||
params.set(textfield.name, textfield.value);
|
||||
});
|
||||
numberfields.forEach((numberfield) => {
|
||||
params.set(numberfield.name, numberfield.value);
|
||||
});
|
||||
selects.forEach(s => params.set(s.name, s.value));
|
||||
ranges.forEach(r => params.set(r.name, r.value));
|
||||
checkboxes.forEach(cb => params.set(cb.name, cb.checked));
|
||||
colorfields.forEach(cf => params.set(cf.name, cf.value));
|
||||
textfields.forEach(tf => params.set(tf.name, tf.value));
|
||||
numberfields.forEach(nf => params.set(nf.name, nf.value));
|
||||
|
||||
document.getElementById("outputUrl").value = baseUrl + '?' + params.toString();
|
||||
document.querySelector('#chat-preview iframe').src = 'chat.html?'+params.toString();
|
||||
outputField.value = baseUrl + '?' + params.toString();
|
||||
document.querySelector('#preview iframe').src = 'chat.html?' + params.toString();
|
||||
}
|
||||
|
||||
async function copyUrl() {
|
||||
|
||||
const output = document.getElementById("outputUrl")
|
||||
/* -------------------------
|
||||
Copiar URL para clipboard
|
||||
-------------------------- */
|
||||
function copyUrl() {
|
||||
const output = document.getElementById("outputUrl");
|
||||
const value = output.value;
|
||||
|
||||
const button = document.querySelector('.url-bar button');
|
||||
const buttonDefaulText = 'Copy URL';
|
||||
|
||||
navigator.clipboard.writeText(value)
|
||||
.then(() => {
|
||||
const buttonDefaultText = 'Copy URL';
|
||||
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
button.textContent = 'ChatRD URL Copied!';
|
||||
button.style.backgroundColor = "#00dd63";
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = buttonDefaulText;
|
||||
button.textContent = buttonDefaultText;
|
||||
button.removeAttribute('style');
|
||||
}, 3000);
|
||||
})
|
||||
.catch(err => {
|
||||
}).catch(err => {
|
||||
console.error("Failed to copy: ", err);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
Mostrar/esconder plataformas
|
||||
-------------------------- */
|
||||
function setupPlatformToggles() {
|
||||
const platforms = document.querySelectorAll('.platform');
|
||||
|
||||
async function setupAddEmoteModal() {
|
||||
platforms.forEach(platform => {
|
||||
const platformId = platform.id;
|
||||
const toggleName = `show${capitalize(platformId)}`;
|
||||
const toggle = platform.querySelector(`input[name="${toggleName}"]`);
|
||||
const setupDiv = platform.querySelector('.setup');
|
||||
|
||||
if (toggle && setupDiv) {
|
||||
// Removido: initializeTransitionStyles(setupDiv);
|
||||
|
||||
// Defina o overflow no CSS ou aqui, se preferir
|
||||
setupDiv.style.overflow = 'hidden';
|
||||
setupDiv.style.transition = 'max-height 0.4s ease, opacity 0.4s ease';
|
||||
|
||||
setVisible(setupDiv, toggle.checked);
|
||||
|
||||
toggle.removeEventListener('change', toggle._handler || (() => { }));
|
||||
|
||||
const handler = () => setVisible(setupDiv, toggle.checked);
|
||||
toggle._handler = handler;
|
||||
toggle.addEventListener('change', handler);
|
||||
}
|
||||
});
|
||||
|
||||
function setVisible(element, visible) {
|
||||
if (visible) {
|
||||
// Remove 'display: none' para que a altura possa ser calculada
|
||||
element.style.display = 'block';
|
||||
|
||||
// Força o elemento a iniciar com altura e opacidade zero
|
||||
element.style.maxHeight = '0px';
|
||||
element.style.opacity = '0';
|
||||
element.offsetHeight; // Força a renderização
|
||||
|
||||
// Inicia a transição para a altura real e opacidade completa
|
||||
element.style.maxHeight = element.scrollHeight + 'px';
|
||||
element.style.opacity = '1';
|
||||
|
||||
// Remove os estilos após a transição de abertura
|
||||
element.addEventListener('transitionend', function handler() {
|
||||
element.style.maxHeight = null;
|
||||
element.style.opacity = null;
|
||||
element.removeEventListener('transitionend', handler);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
else {
|
||||
// Define o maxHeight para a altura atual antes de iniciar a transição de fechamento
|
||||
element.style.maxHeight = element.scrollHeight + 'px';
|
||||
element.offsetHeight; // Força a renderização
|
||||
|
||||
// Inicia a transição para fechar o elemento
|
||||
element.style.maxHeight = '0px';
|
||||
element.style.opacity = '0';
|
||||
|
||||
// Esconde o elemento com 'display: none' após a transição
|
||||
setTimeout(() => {
|
||||
if (element.style.opacity === '0') {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
}, 400); // O tempo precisa ser o mesmo da transição (0.4s)
|
||||
}
|
||||
}
|
||||
|
||||
// A função initializeTransitionStyles() foi removida.
|
||||
// O estilo de transição foi movido para a função principal para ser definido uma única vez.
|
||||
|
||||
function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
Navegação no footer
|
||||
-------------------------- */
|
||||
function setupFooterNavBar() {
|
||||
document.querySelectorAll('.nav-bar a').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('href');
|
||||
if (!targetId || !targetId.startsWith('#')) return;
|
||||
|
||||
const targetElement = document.querySelector(targetId);
|
||||
if (targetElement) {
|
||||
const offset = 20;
|
||||
const y = targetElement.getBoundingClientRect().top + window.scrollY - offset;
|
||||
window.scrollTo({ top: y, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
Modal para adicionar emotes
|
||||
-------------------------- */
|
||||
function setupAddEmoteModal() {
|
||||
const modal = document.getElementById("addEmoteModal");
|
||||
const nameInput = document.getElementById("newEmoteName");
|
||||
const urlInput = document.getElementById("newEmoteURL");
|
||||
const confirmBtn = document.getElementById("confirmAddEmote");
|
||||
const cancelBtn = document.getElementById("cancelAddEmote");
|
||||
const addButton = document.querySelector("#youtube .emote-item:last-child .add");
|
||||
const addButton = document.querySelector("#addEmoteButton");
|
||||
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
|
||||
|
||||
if (!modal || !addButton || !textarea) return;
|
||||
|
||||
// Show modal
|
||||
addButton.onclick = () => {
|
||||
if (streamerBotConnected == true) {
|
||||
// ESC global → aciona cancelBtn
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape" && !modal.classList.contains("hidden")) {
|
||||
cancelBtn.click();
|
||||
}
|
||||
});
|
||||
|
||||
// ENTER nos inputs → aciona confirmBtn
|
||||
[nameInput, urlInput].forEach(input => {
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault(); // evita submit/form
|
||||
confirmBtn.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
addButton.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
if (streamerBotConnected) {
|
||||
nameInput.value = "";
|
||||
urlInput.value = "";
|
||||
modal.classList.remove("hidden");
|
||||
nameInput.focus();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
alert("Streamer.bot is Offline!");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Cancel
|
||||
cancelBtn.onclick = () => {
|
||||
cancelBtn.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
modal.classList.add("hidden");
|
||||
};
|
||||
|
||||
// Confirm
|
||||
confirmBtn.onclick = () => {
|
||||
confirmBtn.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
const name = nameInput.value.trim();
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
@@ -291,27 +317,24 @@ async function setupAddEmoteModal() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add and update
|
||||
emotes[name] = url;
|
||||
textarea.value = JSON.stringify(emotes, null, 4);
|
||||
textarea.value = JSON.stringify(emotes);
|
||||
modal.classList.add("hidden");
|
||||
populateEmoteList();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function populateEmoteList() {
|
||||
/* -------------------------
|
||||
Lista de emotes
|
||||
-------------------------- */
|
||||
function populateEmoteList() {
|
||||
const textarea = document.querySelector("textarea[name=youTubeCustomEmotes]");
|
||||
const emoteList = document.querySelector("#youtube .emote-list");
|
||||
|
||||
if (!textarea || !emoteList) return;
|
||||
|
||||
const addButtonSpan = emoteList.querySelector(".emote-item:last-child");
|
||||
|
||||
// Remove all emote items except the add button
|
||||
emoteList.querySelectorAll(".emote-item").forEach(item => {
|
||||
if (item !== addButtonSpan) {
|
||||
if (item.querySelector("button")?.id !== "addEmoteButton") {
|
||||
item.remove();
|
||||
}
|
||||
});
|
||||
@@ -320,11 +343,12 @@ async function populateEmoteList() {
|
||||
try {
|
||||
emotes = JSON.parse(textarea.value);
|
||||
} catch (e) {
|
||||
console.error("Invalid JSON in YouTube Emotes textarea", e);
|
||||
console.error("[ChatRD][Settings] Invalid JSON in YouTube Emotes textarea", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Recreate each emote item
|
||||
const addButtonSpan = emoteList.querySelector("#addEmoteButton")?.parentElement;
|
||||
|
||||
for (const [emoteName, emoteUrl] of Object.entries(emotes)) {
|
||||
const span = document.createElement("span");
|
||||
span.classList.add("emote-item");
|
||||
@@ -334,119 +358,142 @@ async function populateEmoteList() {
|
||||
<button class="delete"><i class="fa-solid fa-trash-can"></i></button>
|
||||
`;
|
||||
|
||||
// Add delete handler directly to the button
|
||||
const deleteBtn = span.querySelector(".delete");
|
||||
deleteBtn.addEventListener("click", () => {
|
||||
span.querySelector(".delete").addEventListener("click", () => {
|
||||
if (confirm(`Are you sure you want to delete '${emoteName}'?`)) {
|
||||
delete emotes[emoteName];
|
||||
textarea.value = JSON.stringify(emotes, null, 4);
|
||||
populateEmoteList(); // Re-render everything
|
||||
textarea.value = JSON.stringify(emotes);
|
||||
populateEmoteList();
|
||||
}
|
||||
});
|
||||
|
||||
emoteList.insertBefore(span, addButtonSpan);
|
||||
emoteList.insertBefore(span, addButtonSpan || null);
|
||||
}
|
||||
|
||||
setupAddEmoteModal();
|
||||
generateUrl();
|
||||
saveSettingsToLocalStorage();
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
Funções YouTube <-> Streamer.bot
|
||||
-------------------------- */
|
||||
function youtubeSaveMemberEmotes(data) {
|
||||
if (!streamerBotClient) return;
|
||||
const json = JSON.stringify(data);
|
||||
streamerBotClient.doAction({ name: "[YouTube] Member Emotes" }, {
|
||||
"chatrdytcustomemotes": json,
|
||||
}).then((res) => {
|
||||
console.debug('[ChatRD][Settings] Saving YouTube Member Emotes... ', res);
|
||||
});
|
||||
}
|
||||
|
||||
function youtubeLoadMemberEmotes() {
|
||||
if (!streamerBotClient) return Promise.resolve(null);
|
||||
return streamerBotClient.getGlobals().then((globals) => {
|
||||
console.debug('[ChatRD][Settings] Loading Global Vars...', globals);
|
||||
const emoteglobal = globals.variables?.chatrdytcustomemotes;
|
||||
if (!emoteglobal) {
|
||||
console.warn('[ChatRD][Settings] Global variable "chatrdytcustomemotes" not found.');
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(emoteglobal.value);
|
||||
} catch (e) {
|
||||
console.error('[ChatRD][Settings] Failed to parse YouTube Member Emote JSON', e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
Conexão com Streamer.bot
|
||||
-------------------------- */
|
||||
function streamerBotConnect() {
|
||||
const streamerBotStatus = document.getElementById('streamerBotStatus');
|
||||
|
||||
const streamerBotServerAddress = document.querySelector('input[type=text][name=streamerBotServerAddress]').value;
|
||||
const streamerBotServerPort = document.querySelector('input[type=text][name=streamerBotServerPort]').value;
|
||||
|
||||
streamerBotClient = new StreamerbotClient({
|
||||
host: streamerBotServerAddress,
|
||||
port: streamerBotServerPort,
|
||||
onConnect: () => {
|
||||
console.debug(`[ChatRD][Settings] Connected to Streamer.bot successfully!`);
|
||||
streamerBotConnected = true;
|
||||
|
||||
streamerBotStatus.classList.add('connected');
|
||||
streamerBotStatus.querySelector('small').textContent = `Connected`;
|
||||
|
||||
loadSettingsFromLocalStorage();
|
||||
generateUrl();
|
||||
pushChangeEvents();
|
||||
setupFooterNavBar();
|
||||
setupAddEmoteModal();
|
||||
setupPlatformToggles();
|
||||
|
||||
},
|
||||
onDisconnect: () => {
|
||||
streamerBotStatus.classList.remove('connected');
|
||||
streamerBotStatus.querySelector('small').textContent = `Awaiting for connection`;
|
||||
streamerBotConnected = false;
|
||||
console.debug(`[ChatRD][Settings] Streamer.bot Disconnected!`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function speakerBotConnection() {
|
||||
const speakerBotStatus = document.getElementById('speakerBotStatus');
|
||||
|
||||
const speakerBotServerAddress = document.querySelector('input[type=text][name=speakerBotServerAddress]').value;
|
||||
const speakerBotServerPort = document.querySelector('input[type=text][name=speakerBotServerPort]').value;
|
||||
const speakerBotVoiceAlias = document.querySelector('input[type=text][name=speakerBotVoiceAlias]').value;
|
||||
|
||||
const showSpeakerbot = document.querySelector('input[type=checkbox][name=showSpeakerbot]').checked;
|
||||
|
||||
if (!showSpeakerbot) {
|
||||
// Se não é pra mostrar, desconecta caso esteja ativo
|
||||
if (speakerBotClient && speakerBotClient.ws && speakerBotClient.ws.readyState !== WebSocket.CLOSED) {
|
||||
console.log("[ChatRD][Settings] Disconnecting SpeakerBot...");
|
||||
speakerBotClient.disconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Se já está conectado ou conectando, não cria outro
|
||||
if (speakerBotClient && speakerBotClient.ws && speakerBotClient.ws.readyState !== WebSocket.CLOSED) {
|
||||
console.log("[ChatRD][Settings] SpeakerBot WebSocket is already on!.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Cria nova instância
|
||||
speakerBotClient = new SpeakerBotClient({
|
||||
host: speakerBotServerAddress,
|
||||
port: speakerBotServerPort,
|
||||
voiceAlias: speakerBotVoiceAlias,
|
||||
|
||||
onConnect: () => {
|
||||
speakerBotStatus.classList.add('connected');
|
||||
speakerBotStatus.querySelector('small').textContent = `Connected`;
|
||||
},
|
||||
|
||||
onDisconnect: () => {
|
||||
speakerBotStatus.classList.remove('connected');
|
||||
speakerBotStatus.querySelector('small').textContent = `Awaiting for connection`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
const accordionButtons = document.querySelectorAll("button.accordion");
|
||||
|
||||
accordionButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
const targetId = button.getAttribute("data-target");
|
||||
const target = document.getElementById(targetId);
|
||||
const icon = button.querySelector("i");
|
||||
/* -------------------------
|
||||
Inicialização
|
||||
-------------------------- */
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
streamerBotConnect();
|
||||
speakerBotConnection();
|
||||
|
||||
if (!target || !target.classList.contains("accordion-container")) return;
|
||||
|
||||
const isOpen = target.classList.contains("open");
|
||||
|
||||
// Fecha todos os outros accordions
|
||||
document.querySelectorAll(".accordion-container.open").forEach(container => {
|
||||
if (container !== target) {
|
||||
container.classList.remove("open");
|
||||
container.style.maxHeight = null;
|
||||
|
||||
const otherButton = document.querySelector(`button.accordion[data-target="${container.id}"]`);
|
||||
if (otherButton) {
|
||||
const otherIcon = otherButton.querySelector("i");
|
||||
if (otherIcon) otherIcon.className = "fa-solid fa-chevron-down";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Alterna o atual
|
||||
if (!isOpen) {
|
||||
target.classList.add("open");
|
||||
target.style.maxHeight = target.scrollHeight + "px";
|
||||
if (icon) icon.className = "fa-solid fa-chevron-up";
|
||||
|
||||
// Espera a animação terminar para scrollar
|
||||
target.addEventListener("transitionend", function handler(e) {
|
||||
if (e.propertyName === "max-height") {
|
||||
target.removeEventListener("transitionend", handler);
|
||||
|
||||
const offset = target.getBoundingClientRect().top + window.scrollY - 60;
|
||||
window.scrollTo({
|
||||
top: offset,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
else {
|
||||
target.classList.remove("open");
|
||||
target.style.maxHeight = null;
|
||||
if (icon) icon.className = "fa-solid fa-chevron-down";
|
||||
}
|
||||
const speakerBotSwitcher = document.querySelector('input[type=checkbox][name=showSpeakerbot]');
|
||||
speakerBotSwitcher.addEventListener('change', () => {
|
||||
speakerBotConnection();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
loadSettingsFromLocalStorage();
|
||||
generateUrl();
|
||||
pushChangeEvents();
|
||||
populateEmoteList();
|
||||
|
||||
|
||||
document.querySelectorAll('.nav-bar a').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Remove todas as classes dos links dentro da nav-bar
|
||||
document.querySelectorAll('.nav-bar a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
});
|
||||
|
||||
this.classList.add('active');
|
||||
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetElement = document.querySelector(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
|
||||
const offset = 60; // ajusta 20px acima
|
||||
const y = targetElement.getBoundingClientRect().top + window.scrollY - offset;
|
||||
|
||||
window.scrollTo({
|
||||
top: y,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
139
js/speakerbot.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* =============================================================================
|
||||
* SpeakerBotClient
|
||||
* =============================================================================
|
||||
* Author: Rodrigo Emanuel (VortisRD)
|
||||
* Created: 2025-08-10
|
||||
* Description:
|
||||
* WebSocket client for connecting to Speaker.bot, sending TTS (text-to-speech)
|
||||
* messages, and handling reconnection logic automatically.
|
||||
*
|
||||
* Usage Example:
|
||||
* -----------------------------------------------------------------------------
|
||||
* const speakerBot = new SpeakerBotClient({
|
||||
* host: '127.0.0.1',
|
||||
* port: 7580,
|
||||
* voiceAlias: 'Joanna',
|
||||
*
|
||||
* onConnect: () => console.log('Connected!'),
|
||||
* onDisconnect: () => console.log('Disconnected!'),
|
||||
* onError: (err) => console.error(err),
|
||||
* onMessage: (msg) => console.log('SpeakerBot says:', msg)
|
||||
* });
|
||||
*
|
||||
* speakerBot.speak("Hello World!");
|
||||
* -----------------------------------------------------------------------------
|
||||
*
|
||||
* Parameters:
|
||||
* host - IP or hostname of the Speaker.bot server.
|
||||
* port - Port number for the WebSocket connection.
|
||||
* reconnectDelay - Time in ms before attempting reconnection.
|
||||
* voiceAlias - Preferred TTS voice name (string or null).
|
||||
* onConnect - Callback fired when connection is established.
|
||||
* onDisconnect - Callback fired when connection is closed.
|
||||
* onError - Callback fired on connection error.
|
||||
* onMessage - Callback fired when receiving a message from Speaker.bot.
|
||||
*
|
||||
* Dependencies:
|
||||
* None (uses native WebSocket API)
|
||||
*
|
||||
* Notes:
|
||||
* - Messages sent while disconnected are queued and sent after reconnection.
|
||||
* - Bad word filter is enabled by default in speak() payload.
|
||||
*
|
||||
* License:
|
||||
* MIT License
|
||||
* =============================================================================
|
||||
*/
|
||||
|
||||
|
||||
class SpeakerBotClient {
|
||||
constructor({
|
||||
host = '127.0.0.1',
|
||||
port = 7580,
|
||||
reconnectDelay = 10000,
|
||||
voiceAlias = null,
|
||||
onConnect = () => {},
|
||||
onDisconnect = () => {},
|
||||
onError = () => {},
|
||||
onMessage = () => {}
|
||||
} = {}) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.reconnectDelay = reconnectDelay;
|
||||
this.voiceAlias = voiceAlias;
|
||||
this.onConnect = onConnect;
|
||||
this.onDisconnect = onDisconnect;
|
||||
this.onError = onError;
|
||||
this.onMessage = onMessage;
|
||||
|
||||
this.ws = null;
|
||||
this.queue = [];
|
||||
this._manualClose = false; // inicia como false
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
get url() {
|
||||
return `ws://${this.host}:${this.port}/`;
|
||||
}
|
||||
|
||||
get readyState() {
|
||||
return this.ws ? this.ws.readyState : WebSocket.CLOSED;
|
||||
}
|
||||
|
||||
connect() {
|
||||
this._manualClose = false; // reset da flag
|
||||
console.log('[SpeakerBot] Connecting to Speaker.bot...');
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('[SpeakerBot] Connected to Speaker.bot!');
|
||||
this.onConnect();
|
||||
while (this.queue.length > 0) {
|
||||
this.ws.send(this.queue.shift());
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.onMessage(event.data);
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
//console.warn(`[SpeakerBot] Connection error. Reconnecting in ${Math.floor(this.reconnectDelay / 1000)}s...`, error);
|
||||
//this.onError(error);
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.onDisconnect();
|
||||
if (!this._manualClose) {
|
||||
setTimeout(() => this.connect(), this.reconnectDelay);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
speak(message) {
|
||||
const payload = {
|
||||
id: `speak-${Date.now()}`,
|
||||
request: 'Speak',
|
||||
message: message,
|
||||
voice: this.voiceAlias,
|
||||
badWordFilter: true
|
||||
};
|
||||
|
||||
const json = JSON.stringify(payload);
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(json);
|
||||
} else {
|
||||
console.warn(`[SpeakerBot] Not connected yet. Queuing message...`);
|
||||
this.queue.push(json);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._manualClose = true; // flag para evitar reconexão automática
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
}
|