Add files via upload
This commit is contained in:
342
css/app.css
Normal file
342
css/app.css
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
/* 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); }
|
||||||
|
|
||||||
|
/* Full-page container styling */
|
||||||
|
html {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #121212;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message {
|
||||||
|
color: #FFF;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 150%;
|
||||||
|
text-shadow: 2px 2px 2px rgba(0,0,0,0.75);
|
||||||
|
transition: all ease-in-out 300ms;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message img { height: 22px; vertical-align: middle; }
|
||||||
|
|
||||||
|
#chat .message .text img.gigantify {
|
||||||
|
height: 84px;
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message .user { font-weight: bold; }
|
||||||
|
|
||||||
|
#chat .message .time {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(18, 18, 18, 0.5);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message .avatar {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 2px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message .avatar img {
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message .platform {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #000;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-shadow: 2px 2px 2px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message .badges {
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message .badges i {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #000;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 0 5px 0 0;
|
||||||
|
text-shadow: 2px 2px 2px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message .reply {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message .reply i {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#chat .message .shared {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: block;
|
||||||
|
text-shadow: none;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 2px 0;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message .shared span {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 10px 2px 5px;
|
||||||
|
background: #a970ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message .shared i {
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#chat .message.event {
|
||||||
|
text-shadow: 2px 2px 2px rgba(0,0,0,0.25);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message.event .platform {
|
||||||
|
background: none;
|
||||||
|
margin: 2px 7px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message.event > div {
|
||||||
|
padding: 5px 15px 5px 5px;
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: rgba(0,0,0,0.15);
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#chat .message.twitch .platform { background: #a970ff; }
|
||||||
|
#chat .message.twitch .badges img { margin: 0 2px; }
|
||||||
|
#chat .message.twitch.event > div {
|
||||||
|
background: rgb(169,112,255);
|
||||||
|
background: linear-gradient(180deg, rgba(169,112,255,1) 0%, rgba(95,67,138,1) 100%);
|
||||||
|
}
|
||||||
|
#chat .message.twitch.event .platform { background: transparent; }
|
||||||
|
|
||||||
|
#chat .message.twitch.announcement > div {
|
||||||
|
padding: 10px 15px 13px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
border-bottom: 2px solid #ffcc00;
|
||||||
|
background: rgb(18,18,18);
|
||||||
|
background: linear-gradient(180deg, rgba(18,18,18,1) 1%, rgba(0,0,0,1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message.twitch.announcement .reply {
|
||||||
|
padding: 2px;
|
||||||
|
color: #ffcc00;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message.twitch.announcement .reply i {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message.twitch.announcement .platform {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message.twitch.rewards-redemption > div {
|
||||||
|
padding: 10px 15px 13px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
border-bottom: 2px solid #ffcc00;
|
||||||
|
background: rgb(18,18,18);
|
||||||
|
background: linear-gradient(180deg, rgba(18,18,18,1) 1%, rgba(0,0,0,1) 100%);
|
||||||
|
}
|
||||||
|
#chat .message.twitch.rewards-redemption .reply {
|
||||||
|
padding: 2px;
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message.twitch.rewards-redemption .reply i {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
color: #ffcc00;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message.twitch.rewards-redemption .platform {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#chat .message.youtube .platform { background: #ff0000; }
|
||||||
|
#chat .message.youtube.event > div {
|
||||||
|
background: rgb(255,0,0);
|
||||||
|
background: linear-gradient(180deg, rgba(255,0,0,1) 1%, rgba(136,0,0,1) 100%);
|
||||||
|
}
|
||||||
|
#chat .message.youtube.event .platform { background: transparent; }
|
||||||
|
|
||||||
|
|
||||||
|
#chat .message.youtube.owner .user {
|
||||||
|
background: #ffd600;
|
||||||
|
color: #121212 !important;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
text-shadow: 2px 2px 2px rgba(0,0,0,0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat .message.youtube.owner .badges i.fa-solid.fa-video { background: #ff0000 }
|
||||||
|
|
||||||
|
#chat .message.youtube.mod .user { color: #5e84f1 !important; }
|
||||||
|
#chat .message.youtube.mod .badges i.fa-solid.fa-wrench { background: #5e84f1 }
|
||||||
|
|
||||||
|
#chat .message.youtube.sub .user { color: #2ba640 !important; }
|
||||||
|
#chat .message.youtube.sub .badges i.fa-solid.fa-star { background: #2ba640; }
|
||||||
|
|
||||||
|
#chat .message.youtube .badges i.fa-solid.fa-check { background: #999; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#chat .message.streamlabs .platform { background: #80f5d2; }
|
||||||
|
#chat .message.streamlabs.event > div {
|
||||||
|
background: rgb(128,245,210);
|
||||||
|
background: linear-gradient(180deg, rgba(128,245,210,1) 1%, rgba(8,108,78,1) 100%);
|
||||||
|
}
|
||||||
|
#chat .message.streamlabs.event .platform { background: transparent; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#chat .message.streamelements .platform { background: #2700ff; }
|
||||||
|
#chat .message.streamelements.event > div {
|
||||||
|
background: rgb(39,0,255);
|
||||||
|
background: linear-gradient(180deg, rgba(39,0,255,1) 1%, rgba(13,0,88,1) 100%);
|
||||||
|
}
|
||||||
|
#chat .message.streamelements.event .platform { background: transparent; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#chat .message.tiktok .platform { background: #000; }
|
||||||
|
|
||||||
|
|
||||||
|
#chat .message.tiktok.event > div {
|
||||||
|
background: rgb(255,0,80);
|
||||||
|
background: linear-gradient(180deg, rgba(255,0,80,1) 1%, rgba(120,0,38,1) 100%);
|
||||||
|
}
|
||||||
|
#chat .message.tiktok.event .platform { background: transparent; }
|
||||||
|
|
||||||
|
#chat .message.tiktok.mod .badges i.fa-solid.fa-wrench {
|
||||||
|
background: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#statistics {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 11;
|
||||||
|
width: 100%;
|
||||||
|
top: 0; left: 0;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
background: rgba(18, 18, 18, 0.9);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
font-weight: bold;
|
||||||
|
-webkit-mask-image: linear-gradient(to top, transparent, black 40%);
|
||||||
|
mask-image: linear-gradient(to top, transparent, black 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#statistics .platform {
|
||||||
|
background: #000;
|
||||||
|
color: #FFF;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 100px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statistics .platform i.fa-brands {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statistics .platform > span {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statistics .platform#twitch {
|
||||||
|
background: #a970ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statistics .platform#youtube {
|
||||||
|
background: #FF0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statistics .platform#tiktok {
|
||||||
|
background: #ff0050;
|
||||||
|
}
|
248
css/settings.css
Normal file
248
css/settings.css
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
background-color: #121212;
|
||||||
|
color: #FFF;
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { color: #ffcc00; }
|
||||||
|
|
||||||
|
|
||||||
|
#chat-divided {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-settings { width: 640px; }
|
||||||
|
#chat-preview { width: calc(100% - 640px); }
|
||||||
|
|
||||||
|
#chat-preview iframe {
|
||||||
|
position: sticky;
|
||||||
|
top: 30px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-bar {
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-bar input[type=text] {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
border: none;
|
||||||
|
background: #171717;
|
||||||
|
color: #FFF;
|
||||||
|
padding: 15px 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
outline: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.url-bar button {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
padding: 15px 30px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #252525;
|
||||||
|
color: #FFF;
|
||||||
|
transition: .4s;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-bar button:hover {
|
||||||
|
background: #292929;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding: 20px 30px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #171717;
|
||||||
|
margin: 20px auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content h2 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 20px;
|
||||||
|
padding: 15px 0px;
|
||||||
|
border-bottom: 1px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content h2 i {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content .setting {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 5px 0px;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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] {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
border: none;
|
||||||
|
background:#222;
|
||||||
|
color: #FFF;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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=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#extras .setting input[type=checkbox]:checked + .slider {
|
||||||
|
background-color: #5fdd54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content .setting input[type=checkbox]:disabled + .slider {
|
||||||
|
background-color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(18,18,18,0.5);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 10px 5px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #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; }
|
||||||
|
}
|
329
js/app-mockup.js
Normal file
329
js/app-mockup.js
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
/* ----------------------- */
|
||||||
|
/* MOCKUP SYSTEM */
|
||||||
|
/* ----------------------- */
|
||||||
|
|
||||||
|
let mockupInterval = null;
|
||||||
|
let isMockupActive = false;
|
||||||
|
const mockupDelay = 2500; // 2 seconds between events
|
||||||
|
let mockupConnectionState = false; // Track mock connection state
|
||||||
|
|
||||||
|
// Sample data for mockup events
|
||||||
|
const mockData = {
|
||||||
|
users: [
|
||||||
|
{ id: 'user1', name: 'ViewerPro', avatar: 'https://static-cdn.jtvnw.net/user-default-pictures-uv/75305d54-c7cc-40d1-bb9c-91fbe85943c7-profile_image-70x70.png' },
|
||||||
|
{ id: 'user2', name: 'StreamFan42', avatar: 'https://static-cdn.jtvnw.net/user-default-pictures-uv/41780b5a-def8-11e9-94d9-784f43822e80-profile_image-70x70.png' },
|
||||||
|
{ id: 'user3', name: 'ProGamerVIP', avatar: 'https://static-cdn.jtvnw.net/user-default-pictures-uv/dbdc9198-def8-11e9-8681-784f43822e80-profile_image-70x70.png' },
|
||||||
|
{ id: 'user4', name: 'GameQueen', avatar: 'https://static-cdn.jtvnw.net/user-default-pictures-uv/de130ab0-def7-11e9-b668-784f43822e80-profile_image-70x70.png' },
|
||||||
|
],
|
||||||
|
messages: [
|
||||||
|
'Hey everyone! How\'s the stream going?',
|
||||||
|
'This game looks awesome!',
|
||||||
|
'LOL that was hilarious',
|
||||||
|
'GG WP!',
|
||||||
|
'When are you playing Minecraft next?',
|
||||||
|
'Love the new overlay!',
|
||||||
|
'First time watching, this is great!',
|
||||||
|
'Can you explain that strategy again?',
|
||||||
|
'Greetings from Germany!',
|
||||||
|
'What\'s your favorite game?'
|
||||||
|
],
|
||||||
|
emotes: [
|
||||||
|
'<img src="https://static-cdn.jtvnw.net/emoticons/v2/425618/default/dark/1.0" class="emote">',
|
||||||
|
'<img src="https://static-cdn.jtvnw.net/emoticons/v2/425671/default/dark/1.0" class="emote">',
|
||||||
|
'<img src="https://static-cdn.jtvnw.net/emoticons/v2/301428702/default/dark/1.0" class="emote">'
|
||||||
|
],
|
||||||
|
rewards: [
|
||||||
|
'Highlight My Message',
|
||||||
|
'Play Sound Effect',
|
||||||
|
'Choose Next Game',
|
||||||
|
'Song Request',
|
||||||
|
'Dad Joke',
|
||||||
|
'Hydration Check'
|
||||||
|
],
|
||||||
|
announcements: [
|
||||||
|
'Welcome to the stream everyone!',
|
||||||
|
'Don\'t forget to follow for stream notifications!',
|
||||||
|
'We\'re going to raid someone awesome after this game!',
|
||||||
|
'Thanks for all the subs today!',
|
||||||
|
'New emotes coming next week!'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to generate a random mockup event
|
||||||
|
function generateMockEvent() {
|
||||||
|
const eventTypes = [
|
||||||
|
'chat', 'chat', 'chat', 'chat', 'chat', 'chat', 'chat', 'chat', // More weight to regular chat messages
|
||||||
|
'follow', 'sub', 'bits', 'raid', 'superchat', 'gift',
|
||||||
|
'announcement', 'reward', 'resub', 'giftsub'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Select random event type and user
|
||||||
|
const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)];
|
||||||
|
const user = mockData.users[Math.floor(Math.random() * mockData.users.length)];
|
||||||
|
const messageId = createRandomString(40);
|
||||||
|
|
||||||
|
switch(eventType) {
|
||||||
|
case 'chat':
|
||||||
|
// Generate a regular chat message
|
||||||
|
const message = mockData.messages[Math.floor(Math.random() * mockData.messages.length)];
|
||||||
|
// Randomly add an emote
|
||||||
|
const includeEmote = Math.random() > 0.7;
|
||||||
|
const fullMessage = includeEmote ?
|
||||||
|
message + ' ' + mockData.emotes[Math.floor(Math.random() * mockData.emotes.length)] :
|
||||||
|
message;
|
||||||
|
|
||||||
|
const platform = Math.random() > 0.5 ? 'twitch' : (Math.random() > 0.5 ? 'youtube' : 'tiktok');
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: Math.random() > 0.8 ? 'sub' : '',
|
||||||
|
avatar: user.avatar,
|
||||||
|
badges: Math.random() > 0.8 ? '<i class="fa-solid fa-star"></i>' : '',
|
||||||
|
userName: user.name,
|
||||||
|
color: `hsl(${Math.random() * 360}, 100%, 75%)`,
|
||||||
|
message: fullMessage,
|
||||||
|
reply: '',
|
||||||
|
shared: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
addMessageToChat(user.id, messageId, platform, messageData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'follow':
|
||||||
|
const followData = {
|
||||||
|
classes: 'follow',
|
||||||
|
avatar: user.avatar,
|
||||||
|
badges: '',
|
||||||
|
userName: user.name,
|
||||||
|
color: '#FFF',
|
||||||
|
message: currentLang.twitch.follow(),
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(user.id, messageId, 'twitch', followData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sub':
|
||||||
|
const subData = {
|
||||||
|
classes: 'sub',
|
||||||
|
avatar: user.avatar,
|
||||||
|
badges: '',
|
||||||
|
userName: user.name,
|
||||||
|
color: '#FFF',
|
||||||
|
message: currentLang.twitch.sub({
|
||||||
|
months: Math.floor(Math.random() * 24) + 1,
|
||||||
|
isPrime: Math.random() > 0.5,
|
||||||
|
tier: Math.floor(1000 * randomIntFromInterval(1,3))
|
||||||
|
}),
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(user.id, messageId, 'twitch', subData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'resub':
|
||||||
|
const resubData = {
|
||||||
|
classes: 'sub',
|
||||||
|
avatar: user.avatar,
|
||||||
|
badges: '',
|
||||||
|
userName: user.name,
|
||||||
|
color: '#FFF',
|
||||||
|
message: currentLang.twitch.resub({
|
||||||
|
months: Math.floor(Math.random() * 24) + 1,
|
||||||
|
isPrime: Math.random() > 0.5,
|
||||||
|
tier: Math.floor(1000 * randomIntFromInterval(1,3))
|
||||||
|
}),
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(user.id, messageId, 'twitch', resubData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bits':
|
||||||
|
const bitsAmount = Math.floor(Math.random() * 5000) + 100;
|
||||||
|
const bitsData = {
|
||||||
|
classes: 'bits',
|
||||||
|
avatar: user.avatar,
|
||||||
|
badges: '',
|
||||||
|
userName: user.name,
|
||||||
|
color: '#FFF',
|
||||||
|
message: currentLang.twitch.bits({ bits: bitsAmount }),
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(user.id, messageId, 'twitch', bitsData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'raid':
|
||||||
|
const viewerCount = Math.floor(Math.random() * 500) + 10;
|
||||||
|
const raidData = {
|
||||||
|
classes: 'raid',
|
||||||
|
avatar: user.avatar,
|
||||||
|
badges: '',
|
||||||
|
userName: user.name,
|
||||||
|
color: '#FFF',
|
||||||
|
message: currentLang.twitch.raid({ viewers: viewerCount }),
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(user.id, messageId, 'twitch', raidData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'superchat':
|
||||||
|
const amount = (Math.random() * 100 + 5).toFixed(2);
|
||||||
|
const currencies = ['USD', 'EUR', 'CAD', 'GBP', 'AUD'];
|
||||||
|
const currency = currencies[Math.floor(Math.random() * currencies.length)];
|
||||||
|
|
||||||
|
const superChatData = {
|
||||||
|
classes: 'superchat',
|
||||||
|
avatar: user.avatar,
|
||||||
|
badges: '',
|
||||||
|
userName: user.name,
|
||||||
|
color: '#FFF',
|
||||||
|
message: currentLang.youtube.superchat({
|
||||||
|
money: formatCurrency(amount, currency),
|
||||||
|
message: mockData.messages[Math.floor(Math.random() * mockData.messages.length)]
|
||||||
|
}),
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(user.id, messageId, 'youtube', superChatData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gift':
|
||||||
|
const giftData = {
|
||||||
|
classes: 'gift',
|
||||||
|
avatar: user.avatar,
|
||||||
|
badges: '',
|
||||||
|
userName: user.name,
|
||||||
|
color: '#FFF',
|
||||||
|
message: currentLang.tiktok.gift({
|
||||||
|
gift: 'Rose',
|
||||||
|
count: Math.floor(Math.random() * 50) + 1,
|
||||||
|
coins: Math.floor(Math.random() * 1000) + 100
|
||||||
|
}),
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(user.id, messageId, 'tiktok', giftData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'announcement':
|
||||||
|
const announcementText = mockData.announcements[Math.floor(Math.random() * mockData.announcements.length)];
|
||||||
|
const announcementData = {
|
||||||
|
classes: 'announcement',
|
||||||
|
avatar: user.avatar,
|
||||||
|
badges: '',
|
||||||
|
userName: user.name,
|
||||||
|
color: '#FFF',
|
||||||
|
message: ` ${announcementText}`,
|
||||||
|
reply: currentLang.twitch.announcement(),
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(user.id, messageId, 'twitch', announcementData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'reward':
|
||||||
|
const rewardTitle = mockData.rewards[Math.floor(Math.random() * mockData.rewards.length)];
|
||||||
|
const userInput = Math.random() > 0.5 ? mockData.messages[Math.floor(Math.random() * mockData.messages.length)] : '';
|
||||||
|
|
||||||
|
const rewardData = {
|
||||||
|
classes: 'rewards-redemption',
|
||||||
|
avatar: user.avatar,
|
||||||
|
badges: '',
|
||||||
|
userName: user.name,
|
||||||
|
color: '#FFF',
|
||||||
|
message: ` ${userInput}`,
|
||||||
|
reply: currentLang.twitch.channelpoints({ title : rewardTitle }),
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(user.id, messageId, 'twitch', rewardData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'giftsub':
|
||||||
|
const recipientUser = mockData.users[Math.floor(Math.random() * mockData.users.length)];
|
||||||
|
|
||||||
|
const giftsubData = {
|
||||||
|
classes: 'sub',
|
||||||
|
avatar: user.avatar,
|
||||||
|
badges: '',
|
||||||
|
userName: user.name,
|
||||||
|
color: '#FFF',
|
||||||
|
message: currentLang.twitch.gifted({
|
||||||
|
gifted: recipientUser.name,
|
||||||
|
months: Math.floor(Math.random() * 12) + 1,
|
||||||
|
tier: Math.floor(1000 * randomIntFromInterval(1,3)),
|
||||||
|
total: Math.floor(Math.random() * 50) + 1
|
||||||
|
}),
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(user.id, messageId, 'twitch', giftsubData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to start the mockup system
|
||||||
|
function startMockupSystem() {
|
||||||
|
if (!isMockupActive) {
|
||||||
|
console.debug('Starting mockup system...');
|
||||||
|
isMockupActive = true;
|
||||||
|
mockupConnectionState = false;
|
||||||
|
|
||||||
|
// Add a notification about mockup mode
|
||||||
|
notifyInfo({
|
||||||
|
title: currentLang.streamerbotdisconnected || "Streamer.Bot Disconnected",
|
||||||
|
text: "Running in mockup mode. Showing sample events."
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start with a few initial events
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
setTimeout(() => generateMockEvent(), i * 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set interval for regular events
|
||||||
|
mockupInterval = setInterval(generateMockEvent, mockupDelay);
|
||||||
|
|
||||||
|
// Update statistics for demo
|
||||||
|
updateMockStatistics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to stop the mockup system
|
||||||
|
function stopMockupSystem() {
|
||||||
|
if (isMockupActive) {
|
||||||
|
console.debug('Stopping mockup system...');
|
||||||
|
isMockupActive = false;
|
||||||
|
mockupConnectionState = true;
|
||||||
|
clearInterval(mockupInterval);
|
||||||
|
mockupInterval = null;
|
||||||
|
|
||||||
|
// Clear chat to start fresh with real events
|
||||||
|
chatContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to update mock statistics
|
||||||
|
function updateMockStatistics() {
|
||||||
|
if (showPlatformStatistics) {
|
||||||
|
if (showTwitchViewers) {
|
||||||
|
document.querySelector('#statistics #twitch .viewers span').textContent = formatNumber(Math.floor(Math.random() * 500) + 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showYouTubeStatistics) {
|
||||||
|
document.querySelector('#statistics #youtube .viewers span').textContent = formatNumber(Math.floor(Math.random() * 300) + 20);
|
||||||
|
document.querySelector('#statistics #youtube .likes span').textContent = formatNumber(Math.floor(Math.random() * 1000) + 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showTikTokStatistics) {
|
||||||
|
document.querySelector('#statistics #tiktok .viewers span').textContent = formatNumber(Math.floor(Math.random() * 800) + 200);
|
||||||
|
document.querySelector('#statistics #tiktok .likes span').textContent = formatNumber(Math.floor(Math.random() * 5000) + 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function randomIntFromInterval(min, max) { // min and max included
|
||||||
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
|
}
|
287
js/app.js
Normal file
287
js/app.js
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
/* ----------------------- */
|
||||||
|
/* OPTIONS */
|
||||||
|
/* ----------------------- */
|
||||||
|
|
||||||
|
const streamerBotServerAddress = getURLParam("streamerBotServerAddress", "127.0.0.1");
|
||||||
|
const streamerBotServerPort = getURLParam("streamerBotServerPort", "8080");
|
||||||
|
const chatThreshhold = 50;
|
||||||
|
const chatContainer = document.querySelector('#chat');
|
||||||
|
const currentLang = lang[getURLParam("language", 'ptbr')];
|
||||||
|
const showPlatform = getURLParam("showPlatform", false);
|
||||||
|
const showAvatar = getURLParam("showAvatar", false);
|
||||||
|
const showTimestamps = getURLParam("showTimestamps", false);
|
||||||
|
const ampmTimeStamps = getURLParam("ampmTimeStamps", false);
|
||||||
|
const showBadges = getURLParam("showBadges", true);
|
||||||
|
const showPlatformStatistics = getURLParam("showPlatformStatistics", false);
|
||||||
|
const hideAfter = getURLParam("hideAfter", 0);
|
||||||
|
const ignoreChatters = getURLParam("ignoreChatters", "");
|
||||||
|
const excludeCommands = getURLParam("excludeCommands", true);
|
||||||
|
|
||||||
|
const avatars = new Map();
|
||||||
|
const userColors = new Map();
|
||||||
|
|
||||||
|
const ignoreUserList = ignoreChatters.split(',').map(item => item.trim().toLowerCase()) || [];
|
||||||
|
|
||||||
|
/* ----------------------- */
|
||||||
|
/* START */
|
||||||
|
/* ----------------------- */
|
||||||
|
|
||||||
|
if (showPlatformStatistics == false) { document.querySelector('#statistics').style.display = 'none'; }
|
||||||
|
|
||||||
|
/* ----------------------- */
|
||||||
|
/* STREAMER.BOT CONNECTION */
|
||||||
|
/* ----------------------- */
|
||||||
|
|
||||||
|
const streamerBotClient = new StreamerbotClient({
|
||||||
|
host: streamerBotServerAddress,
|
||||||
|
port: streamerBotServerPort,
|
||||||
|
onConnect: (data) => {
|
||||||
|
console.debug( currentLang.streamerbotconnected );
|
||||||
|
console.debug(data);
|
||||||
|
notifySuccess({
|
||||||
|
title: currentLang.streamerbotconnected,
|
||||||
|
text: ``
|
||||||
|
});
|
||||||
|
stopMockupSystem();
|
||||||
|
},
|
||||||
|
onDisconnect: () => {
|
||||||
|
console.error(currentLang.streamerbotdisconnected);
|
||||||
|
startMockupSystem();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
streamerBotClient.on('WebsocketClient.Message', (response) => {
|
||||||
|
console.debug('Event Received:', response);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ----------------------- */
|
||||||
|
/* UTILITIES */
|
||||||
|
/* ----------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function addMessageToChat(userID, messageID, platform, data) {
|
||||||
|
|
||||||
|
const html = DOMPurify.sanitize(`
|
||||||
|
<div id="${messageID}" data-user="${userID}" class="${platform} ${data.classes} message" style="">
|
||||||
|
<div class="animate__animated animate__fadeInUp animate__faster">
|
||||||
|
|
||||||
|
${!data.shared ? '' : data.shared}
|
||||||
|
|
||||||
|
${showTimestamps == true ? '<span class="time">'+whatTimeIsIt()+'</span>' : ''}
|
||||||
|
|
||||||
|
${showPlatform == true ? '<i class="platform fa-brands fa-'+platform+'"></i>' : '' }
|
||||||
|
|
||||||
|
${showAvatar == true ? '<span class="avatar"><img src="'+data.avatar+'"></span>' : ''}
|
||||||
|
|
||||||
|
${showBadges == true ? '<span class="badges">'+data.badges+'</span>' : ''}
|
||||||
|
|
||||||
|
<span style="color: ${data.color}" class="user">${data.userName}:</span>
|
||||||
|
|
||||||
|
${!data.reply ? '' : data.reply}
|
||||||
|
|
||||||
|
<span class="text">${data.message}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
chatContainer.insertAdjacentHTML('beforeend', html);
|
||||||
|
|
||||||
|
const messageElement = document.getElementById(messageID);
|
||||||
|
|
||||||
|
if (hideAfter > 0) {
|
||||||
|
setTimeout(function () {
|
||||||
|
messageElement.style.opacity = 0;
|
||||||
|
setTimeout(function () {
|
||||||
|
messageElement.remove();
|
||||||
|
}, 1000);
|
||||||
|
}, Math.floor(hideAfter * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
removeExtraChatMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function addEventToChat(userID, messageID, platform, data) {
|
||||||
|
|
||||||
|
const html = DOMPurify.sanitize(`
|
||||||
|
<div id="${messageID}" data-user="${userID}" class="${platform} ${data.classes} message event" style="">
|
||||||
|
<div class="animate__animated animate__faster animate__fadeInUp">
|
||||||
|
${!data.reply ? '' : data.reply}
|
||||||
|
|
||||||
|
${showPlatform == true ? '<i class="platform '+(platform == 'money' ? 'fa-solid' : 'fa-brands')+' fa-'+platform+'"></i>' : ' ' }
|
||||||
|
|
||||||
|
<span class="info">
|
||||||
|
<!--<span class="avatar"><img src="${data.avatar}"></span>-->
|
||||||
|
<span style="color: ${data.color}" class="user">${data.userName}</span>
|
||||||
|
<span class="text">${data.message}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
chatContainer.insertAdjacentHTML('beforeend', html);
|
||||||
|
|
||||||
|
const messageElement = document.getElementById(messageID);
|
||||||
|
|
||||||
|
if (hideAfter > 0) {
|
||||||
|
setTimeout(function () {
|
||||||
|
messageElement.style.opacity = 0;
|
||||||
|
setTimeout(function () {
|
||||||
|
messageElement.remove();
|
||||||
|
}, 1000);
|
||||||
|
}, Math.floor(hideAfter * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
removeExtraChatMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const 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 removeExtraChatMessages() {
|
||||||
|
|
||||||
|
const chatMessages = chatContainer.querySelectorAll('div.message').length;
|
||||||
|
|
||||||
|
if (chatMessages > chatThreshhold) {
|
||||||
|
for (let i = 0; i < Math.floor(chatThreshhold/2); i++) {
|
||||||
|
chatContainer.removeChild(chatContainer.firstElementChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
return new Intl.NumberFormat(undefined, {
|
||||||
|
style: 'currency',
|
||||||
|
currency: currencyCode,
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
}).format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 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 getURLParam(param, defaultValue) {
|
||||||
|
const chatQueryString = window.location.search;
|
||||||
|
const urlParams = new URLSearchParams(chatQueryString);
|
||||||
|
const paramVar = urlParams.get(param);
|
||||||
|
|
||||||
|
switch (paramVar) {
|
||||||
|
case 'true':
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'false':
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case null:
|
||||||
|
case undefined:
|
||||||
|
return defaultValue;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return paramVar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
72
js/lang/en.js
Normal file
72
js/lang/en.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
const en = {
|
||||||
|
streamerbotconnected: 'Streamer.bot Online!',
|
||||||
|
streamerbotdisconnected: 'Streamer.bot Disconnected!',
|
||||||
|
|
||||||
|
twitch : {
|
||||||
|
follow : () => ` followed the channel.`,
|
||||||
|
announcement : () => ` <div class="reply">📢 <strong>Announcement</strong></div>`,
|
||||||
|
channelpoints : ({ title }) => ` <div class="reply"><i class="fa-solid fa-wand-magic-sparkles"></i> <strong>Channel Points - ${title}</strong></div>`,
|
||||||
|
bits : ({ bits, message }) => ` cheered <i class="fa-regular fa-gem fall-and-bounce"></i> <strong>${bits} bits</strong>${message ? ' - '+message : ''}`,
|
||||||
|
|
||||||
|
sub : ({ months, isPrime, tier }) => ` subscribed for
|
||||||
|
${isPrime == true ? '<i class="fa-solid fa-crown"></i>' : '<i class="fa-solid fa-star"></i>'}
|
||||||
|
<strong>${months || 1 } ${months == 1 ? 'month' : 'months'}
|
||||||
|
(${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})</strong>`,
|
||||||
|
|
||||||
|
resub : ({ months, isPrime, tier }) => ` subscribed for
|
||||||
|
${isPrime == true ? '<i class="fa-solid fa-crown"></i>' : '<i class="fa-solid fa-star"></i>'}
|
||||||
|
<strong>${months || 1 } ${months == 1 ? 'month' : 'months'}
|
||||||
|
(${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})</strong>`,
|
||||||
|
|
||||||
|
gifted : ({ gifted, months, tier, total }) => ` gifted
|
||||||
|
<strong>${months || 1 } ${months == 1 ? 'month' : 'months'}
|
||||||
|
of Tier ${tier.toString().charAt(0)} sub</strong>
|
||||||
|
to <i class="fa-solid fa-gift"></i> <strong>${gifted}</strong>`,
|
||||||
|
|
||||||
|
giftedbomb : ({ count, total, tier }) => ` gifted <i class="fa-solid fa-gift"></i> <strong>${count} Tier ${tier.toString().charAt(0)} subs</strong> to the Community, <strong>${total || 1} ${total == 1 ? 'gift' : 'gifts'} in total.</strong>`,
|
||||||
|
|
||||||
|
raid : ({ viewers }) => ` raided the channel with <i class="fa-solid fa-users"></i> <strong>${viewers} viewers</strong>`
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
youtube : {
|
||||||
|
superchat : ({ money, message }) => ` superchatted <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
|
||||||
|
${message ? ' - '+message : ''}
|
||||||
|
`,
|
||||||
|
|
||||||
|
supersticker : ({ money, message }) => ` sent a supersticker of <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
|
||||||
|
${message ? ' - '+message : ''}
|
||||||
|
`,
|
||||||
|
|
||||||
|
member : ({ months, tier }) => ` became a member for
|
||||||
|
<i class="fa-solid fa-star"></i>
|
||||||
|
<strong>${months || 1 } ${months == 1 ? 'month' : 'months'}
|
||||||
|
(Tier ${tier})</strong>`,
|
||||||
|
|
||||||
|
giftedmembers : ({ total, tier }) => ` gifted <i class="fa-solid fa-gift"></i> <strong>${total} Tier ${tier} memberships</strong>`,
|
||||||
|
|
||||||
|
giftedtrainmembers : ({ gifted, tier }) => ` gifted a membership
|
||||||
|
<strong>(${tier})</strong>
|
||||||
|
to <i class="fa-solid fa-gift"></i> <strong>${gifted}</strong>`,
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
streamlabs : {
|
||||||
|
tip : ({ money, message }) => ` donated 🪙 <strong>${money}</strong><!-- ${message} -->`,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
streamelements : {
|
||||||
|
tip : ({ money, message }) => ` donated 🪙 <strong>${money}</strong><!-- ${message} -->`,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
tiktok : {
|
||||||
|
follow : () => ` followed the channel.`,
|
||||||
|
sub : ({ months }) => ` subscribed for <i class="fa-solid fa-star"></i> <strong>${months || 1 } ${months == 1 ? 'month' : 'months'}.</strong>`,
|
||||||
|
gift : ({ gift, count, coins }) => ` gifted <strong>${gift} x${count}</strong> (🪙 <strong>${coins} ${coins == 1 ? 'coin' : 'coins'}).</strong>`,
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
4
js/lang/lang.js
Normal file
4
js/lang/lang.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const lang = {
|
||||||
|
ptbr : ptbr,
|
||||||
|
en : en
|
||||||
|
}
|
72
js/lang/ptbr.js
Normal file
72
js/lang/ptbr.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
const ptbr = {
|
||||||
|
streamerbotconnected: 'Streamer.bot Conectado!',
|
||||||
|
streamerbotdisconnected: 'Streamer.bot Desconectado!',
|
||||||
|
|
||||||
|
twitch : {
|
||||||
|
follow : () => ` seguiu o canal.`,
|
||||||
|
announcement : () => ` <div class="reply">📢 <strong>Anúncio</strong></div>`,
|
||||||
|
channelpoints : ({ title }) => ` <div class="reply"><i class="fa-solid fa-wand-magic-sparkles"></i> <strong>Pontos do Canal - ${title}</strong></div>`,
|
||||||
|
bits : ({ bits, message }) => ` doou <i class="fa-regular fa-gem fall-and-bounce"></i> <strong>${bits} bits</strong>${message ? ' - '+message : ''}`,
|
||||||
|
|
||||||
|
sub : ({ months, isPrime, tier }) => ` se inscreveu por
|
||||||
|
${isPrime == true ? '<i class="fa-solid fa-crown"></i>' : '<i class="fa-solid fa-star"></i>'}
|
||||||
|
<strong>${months || 1 } ${months == 1 ? 'mês' : 'meses'}
|
||||||
|
(${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})</strong>`,
|
||||||
|
|
||||||
|
resub : ({ months, isPrime, tier }) => ` se inscreveu por
|
||||||
|
${isPrime == true ? '<i class="fa-solid fa-crown"></i>' : '<i class="fa-solid fa-star"></i>'}
|
||||||
|
<strong>${months || 1 } ${months == 1 ? 'mês' : 'meses'}
|
||||||
|
(${isPrime == true ? 'Prime' : 'Tier '+tier.toString().charAt(0)})</strong>`,
|
||||||
|
|
||||||
|
gifted : ({ gifted, months, tier }) => ` doou
|
||||||
|
<strong>${months || 1 } ${months == 1 ? 'mês' : 'meses'}
|
||||||
|
de Tier ${tier.toString().charAt(0)}</strong>
|
||||||
|
para <i class="fa-solid fa-gift"></i> <strong>${gifted}</strong>`,
|
||||||
|
|
||||||
|
giftedbomb : ({ count, total, tier }) => ` doou <i class="fa-solid fa-gift"></i> <strong>${count} inscrições Tier ${tier.toString().charAt(0)}</strong> para a Comunidade, totalizando <strong>${total || 1} ${total == 1 ? 'doação' : 'doações'}.</strong>`,
|
||||||
|
|
||||||
|
raid : ({ viewers }) => ` raidou o canal com <i class="fa-solid fa-users"></i> <strong>${viewers} pessoas</strong>`
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
youtube : {
|
||||||
|
superchat : ({ money, message }) => ` fez um superchat de <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
|
||||||
|
${message ? ' - '+message : ''}
|
||||||
|
`,
|
||||||
|
|
||||||
|
supersticker : ({ money, message }) => ` enviou um superchat de <i class="fa-solid fa-comments-dollar"></i> <strong>${money}</strong>
|
||||||
|
${message ? ' - '+message : ''}
|
||||||
|
`,
|
||||||
|
|
||||||
|
member : ({ months, tier }) => ` se inscreveu por
|
||||||
|
<i class="fa-solid fa-star"></i>
|
||||||
|
<strong>${months || 1 } ${months == 1 ? 'mês' : 'meses'}
|
||||||
|
(Tier ${tier})</strong>`,
|
||||||
|
|
||||||
|
giftedmembers : ({ total, tier }) => ` doou <i class="fa-solid fa-gift"></i> <strong>${total} inscrições (Tier ${tier})</strong>`,
|
||||||
|
|
||||||
|
giftedtrainmembers : ({ gifted, tier }) => ` doou uma assinatura
|
||||||
|
<strong>(${tier})</strong>
|
||||||
|
para <i class="fa-solid fa-gift"></i> <strong>${gifted}</strong>`,
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
streamlabs : {
|
||||||
|
tip : ({ money, message }) => ` doou 🪙 <strong>${money}</strong><!-- ${message} -->`,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
streamelements : {
|
||||||
|
tip : ({ money, message }) => ` doou 🪙 <strong>${money}</strong><!-- ${message} -->`,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
tiktok : {
|
||||||
|
follow : () => ` seguiu o canal.`,
|
||||||
|
sub : ({ months }) => ` se inscreveu por <i class="fa-solid fa-star"></i> <strong>${months || 1 } ${months == 1 ? 'mês' : 'meses'}.</strong>`,
|
||||||
|
gift : ({ gift, count, coins }) => ` doou <strong>${gift} x${count}</strong> (🪙 <strong>${coins} ${coins == 1 ? 'moeda' : 'moedas'}).</strong>`,
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
139
js/settings.js
Normal file
139
js/settings.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
function saveSettingsToLocalStorage() {
|
||||||
|
const checkboxes = document.querySelectorAll("input[type=checkbox]");
|
||||||
|
const textfields = document.querySelectorAll("input[type=text]");
|
||||||
|
const numberfields = document.querySelectorAll("input[type=number]");
|
||||||
|
const selects = document.querySelectorAll("select");
|
||||||
|
|
||||||
|
const settings = {};
|
||||||
|
|
||||||
|
checkboxes.forEach((checkbox) => {
|
||||||
|
settings[checkbox.name] = checkbox.checked;
|
||||||
|
});
|
||||||
|
textfields.forEach((textfield) => {
|
||||||
|
settings[textfield.name] = textfield.value;
|
||||||
|
});
|
||||||
|
numberfields.forEach((numberfield) => {
|
||||||
|
settings[numberfield.name] = numberfield.value;
|
||||||
|
});
|
||||||
|
selects.forEach((select) => {
|
||||||
|
settings[select.name] = select.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem("chatWidgetSettings", JSON.stringify(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadSettingsFromLocalStorage() {
|
||||||
|
const saved = localStorage.getItem("chatWidgetSettings");
|
||||||
|
if (!saved) return;
|
||||||
|
|
||||||
|
const settings = JSON.parse(saved);
|
||||||
|
|
||||||
|
Object.keys(settings).forEach((key) => {
|
||||||
|
const input = document.querySelector(`[name="${key}"]`);
|
||||||
|
if (input) {
|
||||||
|
if (input.type === "checkbox") {
|
||||||
|
input.checked = settings[key];
|
||||||
|
} else {
|
||||||
|
input.value = settings[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function pushChangeEvents() {
|
||||||
|
const checkboxes = document.querySelectorAll("input[type=checkbox]");
|
||||||
|
const textfields = document.querySelectorAll("input[type=text]");
|
||||||
|
const numberfields = document.querySelectorAll("input[type=number]");
|
||||||
|
const selects = document.querySelectorAll("select");
|
||||||
|
|
||||||
|
checkboxes.forEach((checkbox) => {
|
||||||
|
checkbox.addEventListener('change', () => {
|
||||||
|
generateUrl();
|
||||||
|
saveSettingsToLocalStorage();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
textfields.forEach((textfield) => {
|
||||||
|
textfield.addEventListener('input', () => {
|
||||||
|
generateUrl();
|
||||||
|
saveSettingsToLocalStorage();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
numberfields.forEach((numberfield) => {
|
||||||
|
numberfield.addEventListener('input', () => {
|
||||||
|
generateUrl();
|
||||||
|
saveSettingsToLocalStorage();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
selects.forEach((select) => {
|
||||||
|
select.addEventListener('change', () => {
|
||||||
|
generateUrl();
|
||||||
|
saveSettingsToLocalStorage();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function generateUrl() {
|
||||||
|
document.getElementById("outputUrl").value = '';
|
||||||
|
|
||||||
|
var runThisLocally = document.querySelector("input[type=checkbox][name=runThisLocally]").checked;
|
||||||
|
console.log(runThisLocally);
|
||||||
|
var baseUrl = '';
|
||||||
|
|
||||||
|
if (runThisLocally == false) {
|
||||||
|
baseUrl = 'https://vortisrd.github.io/chatrd/chat.html'
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkboxes = document.querySelectorAll("input[type=checkbox]");
|
||||||
|
const textfields = document.querySelectorAll("input[type=text]");
|
||||||
|
const numberfields = document.querySelectorAll("input[type=number]");
|
||||||
|
const selects = document.querySelectorAll("select");
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
|
||||||
|
selects.forEach((select) => {
|
||||||
|
params.set(select.name, select.value);
|
||||||
|
});
|
||||||
|
checkboxes.forEach((checkbox) => {
|
||||||
|
params.set(checkbox.name, checkbox.checked);
|
||||||
|
});
|
||||||
|
textfields.forEach((textfield) => {
|
||||||
|
params.set(textfield.name, textfield.value);
|
||||||
|
});
|
||||||
|
numberfields.forEach((numberfield) => {
|
||||||
|
params.set(numberfield.name, numberfield.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("outputUrl").value = baseUrl + '?' + params.toString();
|
||||||
|
document.querySelector('#chat-preview iframe').src = 'chat.html?'+params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyUrl() {
|
||||||
|
|
||||||
|
const output = document.getElementById("outputUrl");
|
||||||
|
|
||||||
|
output.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
|
||||||
|
const button = document.querySelector('.url-bar button');
|
||||||
|
const buttonDefaulText = 'Copy URL';
|
||||||
|
|
||||||
|
button.textContent = 'ChatRD URL Copied!';
|
||||||
|
button.style.backgroundColor = "#00dd63";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.textContent = buttonDefaulText;
|
||||||
|
button.removeAttribute('style');
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
loadSettingsFromLocalStorage();
|
||||||
|
generateUrl();
|
||||||
|
pushChangeEvents();
|
||||||
|
});
|
44
js/streamelements/module.js
Normal file
44
js/streamelements/module.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const showStreamElementsTips = getURLParam("showStreamElementsTips", true);
|
||||||
|
|
||||||
|
const streamElementsHandlers = {
|
||||||
|
'StreamElements.Tip': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showStreamElementsTips == false)
|
||||||
|
return;
|
||||||
|
streamElementsEventMessage(response.data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
for (const [event, handler] of Object.entries(streamElementsHandlers)) {
|
||||||
|
streamerBotClient.on(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function streamElementsEventMessage(data) {
|
||||||
|
const {
|
||||||
|
username: userName,
|
||||||
|
amount: moneyFromUser,
|
||||||
|
currency: currencyFromUser,
|
||||||
|
message: messageFromUser,
|
||||||
|
} = data;
|
||||||
|
const userID = createRandomString(40);
|
||||||
|
const messageID = createRandomString(40);
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
'',
|
||||||
|
currentLang.streamlabs.tip({
|
||||||
|
money : formatCurrency(moneyFromUser,currencyFromUser),
|
||||||
|
message : messageFromUser
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
const classes = 'streamelements';
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
addEventToChat(userID, messageID, 'dollar-sign', messageData);
|
||||||
|
}
|
42
js/streamlabs/module.js
Normal file
42
js/streamlabs/module.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const showStreamlabsDonations = getURLParam("showStreamlabsDonations", true);
|
||||||
|
|
||||||
|
const streamLabsHandlers = {
|
||||||
|
'Streamlabs.Donation': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showStreamlabsDonations == false)
|
||||||
|
return;
|
||||||
|
streamLabsEventMessage(response.data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
for (const [event, handler] of Object.entries(streamLabsHandlers)) {
|
||||||
|
streamerBotClient.on(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function streamLabsEventMessage(data) {
|
||||||
|
const {
|
||||||
|
from: userName,
|
||||||
|
formattedAmount: moneyFromUser,
|
||||||
|
currency: currencyFromUser,
|
||||||
|
message: messageFromUser,
|
||||||
|
} = data;
|
||||||
|
const userID = createRandomString(40);
|
||||||
|
const messageID = createRandomString(40);
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
'',
|
||||||
|
currentLang.streamlabs.tip({
|
||||||
|
money : formatCurrency(moneyFromUser,currencyFromUser),
|
||||||
|
message : messageFromUser
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
const classes = 'streamlabs';
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
addEventToChat(userID, messageID, 'dollar-sign', messageData);
|
||||||
|
}
|
222
js/tiktok/module.js
Normal file
222
js/tiktok/module.js
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
/* ----------------------------------------------------------------------------------------- */
|
||||||
|
/* TikFinity >< Streamer.Bot */
|
||||||
|
/* ----------------------------------------------------------------------------------------- */
|
||||||
|
/* In Streamer.bot, go into Servers/Clients, then Websocket clients, */
|
||||||
|
/* and add the server info for TikFinity Desktop App. */
|
||||||
|
/* ----------------------------------------------------------------------------------------- */
|
||||||
|
/* If it's also running on the same computer, the address will be: ws://127.0.0.1:21213/ */
|
||||||
|
/* ----------------------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const showTikTokMessages = getURLParam("showTikTokMessages", true);
|
||||||
|
const showTikTokFollows = getURLParam("showTikTokFollows", true);
|
||||||
|
const showTikTokGifts = getURLParam("showTikTokGifts", true);
|
||||||
|
const showTikTokSubs = getURLParam("showTikTokSubs", true);
|
||||||
|
const showTikTokStatistics = getURLParam("showTikTokStatistics", true);
|
||||||
|
|
||||||
|
userColors.set('tiktok', new Map());
|
||||||
|
|
||||||
|
if (showTikTokStatistics == false) { document.querySelector('#statistics #tiktok').style.display = 'none'; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
streamerBotClient.on('General.Custom', (response) => {
|
||||||
|
if (response.data.platform === 'TikTok') {
|
||||||
|
|
||||||
|
let json = response.data;
|
||||||
|
let jsonData = json.data.data;
|
||||||
|
|
||||||
|
switch (json.data.event) {
|
||||||
|
case 'roomUser' :
|
||||||
|
if (showPlatformStatistics == false || showTikTokStatistics == false) return;
|
||||||
|
tiktokUpdateStatistics(jsonData, 'viewers');
|
||||||
|
break;
|
||||||
|
case 'like' :
|
||||||
|
if (showPlatformStatistics == false || showTikTokStatistics == false) return;
|
||||||
|
tiktokUpdateStatistics(jsonData, 'likes');
|
||||||
|
break;
|
||||||
|
case 'chat' :
|
||||||
|
console.debug(json);
|
||||||
|
if (showTikTokMessages == false) return;
|
||||||
|
if (ignoreUserList.includes(jsonData.nickname.toLowerCase())) return;
|
||||||
|
tiktokChatMessage(jsonData);
|
||||||
|
break;
|
||||||
|
case 'follow' :
|
||||||
|
if (showTikTokFollows == false) return;
|
||||||
|
tiktokFollowMessage(jsonData);
|
||||||
|
break;
|
||||||
|
case 'subscribe' :
|
||||||
|
if (showTikTokSubs == false) return;
|
||||||
|
tiktokSubMessage(jsonData);
|
||||||
|
break;
|
||||||
|
case 'gift' :
|
||||||
|
if (showTikTokGifts == false) return;
|
||||||
|
if (jsonData.giftType === 1 && !jsonData.repeatEnd) {}
|
||||||
|
else {
|
||||||
|
tiktokGiftMessage(jsonData);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
//console.debug(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function tiktokChatMessage(data) {
|
||||||
|
if (data.comment.startsWith("!") && excludeCommands == true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
const {
|
||||||
|
userId: userID,
|
||||||
|
msgId: messageID,
|
||||||
|
profilePictureUrl: avatar,
|
||||||
|
comment: message,
|
||||||
|
emotes,
|
||||||
|
nickname: userName,
|
||||||
|
isSubscriber,
|
||||||
|
isModerator,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const badgesHTML = [
|
||||||
|
isSubscriber && '<i class="fa-solid fa-star"></i>',
|
||||||
|
isModerator && '<i class="fa-solid fa-user-gear"></i>',
|
||||||
|
].filter(Boolean).join('');
|
||||||
|
|
||||||
|
const classes = [
|
||||||
|
isSubscriber && 'sub',
|
||||||
|
isModerator && 'mod',
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
var fullmessage = message;
|
||||||
|
|
||||||
|
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('');
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes.join(' '),
|
||||||
|
avatar,
|
||||||
|
badges: badgesHTML,
|
||||||
|
userName,
|
||||||
|
color: await createRandomColor('tiktok', userID),
|
||||||
|
message: fullmessage,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addMessageToChat(userID, messageID, 'tiktok', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function tiktokFollowMessage(data) {
|
||||||
|
const {
|
||||||
|
userId: userID,
|
||||||
|
msgId: messageID,
|
||||||
|
profilePictureUrl: avatar,
|
||||||
|
nickname: userName,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const message = currentLang.tiktok.follow();
|
||||||
|
const classes = 'follow'
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'tiktok', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function tiktokSubMessage(data) {
|
||||||
|
const {
|
||||||
|
userId: userID,
|
||||||
|
msgId: messageID,
|
||||||
|
profilePictureUrl: avatar,
|
||||||
|
nickname: userName,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const message = currentLang.tiktok.sub({
|
||||||
|
months : data.subMonth
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = 'sub'
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'tiktok', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function tiktokGiftMessage(data) {
|
||||||
|
const {
|
||||||
|
userId: userID,
|
||||||
|
msgId: messageID,
|
||||||
|
profilePictureUrl: avatar,
|
||||||
|
nickname: userName,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
var coins = Math.floor(data.repeatCount*data.diamondCount);
|
||||||
|
|
||||||
|
const message = currentLang.tiktok.gift({
|
||||||
|
gift : data.giftName,
|
||||||
|
count : data.repeatCount,
|
||||||
|
coins : coins
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = 'gift'
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'tiktok', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function tiktokUpdateStatistics(data, type) {
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
586
js/twitch/module.js
Normal file
586
js/twitch/module.js
Normal file
@@ -0,0 +1,586 @@
|
|||||||
|
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 showTwitchViewers = getURLParam("showTwitchViewers", true);
|
||||||
|
|
||||||
|
|
||||||
|
if (showTwitchViewers == false) { document.querySelector('#statistics #twitch').style.display = 'none'; }
|
||||||
|
|
||||||
|
|
||||||
|
const twitchMessageHandlers = {
|
||||||
|
'Twitch.ChatMessage': (response) => {
|
||||||
|
console.debug('Twitch Chat', response.data);
|
||||||
|
if (showTwitchMessages == false)
|
||||||
|
return;
|
||||||
|
if (ignoreUserList.includes(response.data.message.username.toLowerCase()))
|
||||||
|
return;
|
||||||
|
twitchChatMessage(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.Follow': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showTwitchFollows == false)
|
||||||
|
return;
|
||||||
|
twitchFollowMessage(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.Announcement': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showTwitchAnnouncements == false) return;
|
||||||
|
twitchAnnouncementMessage(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.Cheer': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showTwitchBits == false) return;
|
||||||
|
twitchBitsMessage(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.AutomaticRewardRedemption': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showTwitchMessages == false) return;
|
||||||
|
twitchChatMessageGiantEmote(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.RewardRedemption': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showTwitchRewardRedemptions == false) return;
|
||||||
|
twitchRewardRedemption(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.Sub': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showTwitchSubs == false) return;
|
||||||
|
twitchSubMessage(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.ReSub': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showTwitchSubs == false) return;
|
||||||
|
twitchReSubMessage(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.GiftSub': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (response.data.fromCommunitySubGift === false) {
|
||||||
|
if (showTwitchSubs == false || showTwitchGiftedSubs == false) return;
|
||||||
|
twitchGiftMessage(response.data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (showTwitchSubs == false || showTwitchGiftedSubsUserTrain == false) return;
|
||||||
|
twitchGiftMessage(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
'Twitch.GiftBomb': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showTwitchSubs == false || showTwitchMassGiftedSubs == false) return;
|
||||||
|
twitchGiftSubsMessage(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.Raid': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showTwitchRaids == false) return;
|
||||||
|
twitchRaidMessage(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.ChatMessageDeleted': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
twitchChatMessageDeleted(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.UserBanned': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
twitchUserBanned(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.UserTimedOut': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
twitchUserBanned(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.ViewerCountUpdate': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showPlatformStatistics == false || showTwitchViewers == false) return;
|
||||||
|
twitchUpdateStatistics(response.data);
|
||||||
|
},
|
||||||
|
'Twitch.ChatCleared': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
twitchChatClearMessages();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [event, handler] of Object.entries(twitchMessageHandlers)) {
|
||||||
|
streamerBotClient.on(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function twitchChatMessage(data) {
|
||||||
|
if (data.message.message.startsWith("!") && excludeCommands == true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const {
|
||||||
|
message: {
|
||||||
|
username: userID,
|
||||||
|
color,
|
||||||
|
displayName: userName,
|
||||||
|
message: text,
|
||||||
|
firstMessage,
|
||||||
|
isReply,
|
||||||
|
isSharedChat,
|
||||||
|
reply: replyData,
|
||||||
|
},
|
||||||
|
messageId,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const [avatar, message, badges] = await Promise.all([
|
||||||
|
getTwitchAvatar(userID),
|
||||||
|
getTwitchEmotes(data),
|
||||||
|
getTwitchBadges(data),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const classes = firstMessage ? ['first-message'] : [];
|
||||||
|
const replyHTML = isReply ?
|
||||||
|
`<div class="reply"><i class="fa-solid fa-arrow-turn-up"></i> <strong>${replyData.userName}:</strong> ${replyData.msgBody}</div>` :
|
||||||
|
'';
|
||||||
|
|
||||||
|
var sharedChat = '';
|
||||||
|
|
||||||
|
if (isSharedChat) {
|
||||||
|
if (showTwitchSharedChat == true) {
|
||||||
|
if (!data.sharedChat.primarySource)
|
||||||
|
{
|
||||||
|
var sharedChat = `<div class="shared"><span><i class="fa-solid fa-comments"></i> <strong>${data.sharedChat.sourceRoom.name}</strong></span> <i class="fa-solid fa-arrow-turn-down"></i></div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!data.sharedChat.primarySource && showTwitchSharedChat == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes.join(' '),
|
||||||
|
avatar,
|
||||||
|
badges,
|
||||||
|
userName,
|
||||||
|
color,
|
||||||
|
message,
|
||||||
|
shared: sharedChat,
|
||||||
|
reply: replyHTML,
|
||||||
|
};
|
||||||
|
|
||||||
|
addMessageToChat(userID, messageId, 'twitch', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function twitchChatMessageGiantEmote(data) {
|
||||||
|
const { user_login: userID, gigantified_emote: { id: emoteGigantify } } = data;
|
||||||
|
|
||||||
|
const userMessages = chatContainer.querySelectorAll(`.twitch.message[data-user="${userID}"]`);
|
||||||
|
if (userMessages.length === 0) return;
|
||||||
|
|
||||||
|
const firstMessage = userMessages[0];
|
||||||
|
const emoteImages = firstMessage.querySelectorAll(`img[data-emote-id="${emoteGigantify}"]`);
|
||||||
|
if (emoteImages.length === 0) return;
|
||||||
|
|
||||||
|
emoteImages.forEach(img => {
|
||||||
|
img.classList.add("gigantify");
|
||||||
|
if (img.src.endsWith("2.0")) {
|
||||||
|
img.src = img.src.replace("2.0", "3.0");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function twitchFollowMessage(data) {
|
||||||
|
const {
|
||||||
|
user_id : userID,
|
||||||
|
user_name : userName
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const messageID = createRandomString(40);
|
||||||
|
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
getTwitchAvatar(userID),
|
||||||
|
currentLang.twitch.follow(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const classes = 'follow';
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'twitch', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function twitchBitsMessage(data) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
messageId : messageID,
|
||||||
|
user : {
|
||||||
|
id : userID,
|
||||||
|
name : userName
|
||||||
|
}
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
getTwitchAvatar(userID),
|
||||||
|
currentLang.twitch.bits({
|
||||||
|
bits: data.message.bits,
|
||||||
|
message : data.message.message.replace(/\bCheer\d+\b/g, '').replace(/\s+/g, ' ').trim()
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const classes = 'bits';
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'twitch', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function twitchAnnouncementMessage(data) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
messageId : messageID,
|
||||||
|
user : {
|
||||||
|
id : userID,
|
||||||
|
name : userName
|
||||||
|
}
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
data.message = {
|
||||||
|
message: await getTwitchAnnouncementEmotes(data)
|
||||||
|
};
|
||||||
|
|
||||||
|
const replyHTML = currentLang.twitch.announcement();
|
||||||
|
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
getTwitchAvatar(userID),
|
||||||
|
` ${data.message.message}`
|
||||||
|
]);
|
||||||
|
|
||||||
|
const classes = 'announcement';
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: replyHTML,
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'twitch', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function twitchRewardRedemption(data) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
user_id : userID,
|
||||||
|
user_name : userName,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const messageID = createRandomString(40);
|
||||||
|
|
||||||
|
const replyHTML = currentLang.twitch.channelpoints({ title : data.reward.title });
|
||||||
|
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
getTwitchAvatar(userID),
|
||||||
|
` ${data.user_input}`
|
||||||
|
]);
|
||||||
|
|
||||||
|
const classes = 'rewards-redemption';
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: replyHTML,
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'twitch', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function twitchSubMessage(data) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
user : {
|
||||||
|
id : userID,
|
||||||
|
name : userName
|
||||||
|
}
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const messageID = createRandomString(40);
|
||||||
|
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
getTwitchAvatar(userID),
|
||||||
|
currentLang.twitch.sub({
|
||||||
|
months : data.duration_months,
|
||||||
|
isPrime : data.isPrime,
|
||||||
|
tier : data.sub_tier
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const classes = 'sub';
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'twitch', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function twitchReSubMessage(data) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
user : {
|
||||||
|
id : userID,
|
||||||
|
name : userName
|
||||||
|
}
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const messageID = createRandomString(40);
|
||||||
|
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
getTwitchAvatar(userID),
|
||||||
|
currentLang.twitch.resub({
|
||||||
|
months : data.cumulativeMonths,
|
||||||
|
isPrime : data.isPrime,
|
||||||
|
tier : data.subTier
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const classes = 'sub';
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'twitch', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function twitchGiftMessage(data) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
user : {
|
||||||
|
id : userID,
|
||||||
|
name : userName
|
||||||
|
}
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const messageID = createRandomString(40);
|
||||||
|
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
getTwitchAvatar(userID),
|
||||||
|
currentLang.twitch.gifted({
|
||||||
|
gifted : data.recipient.name,
|
||||||
|
months : data.durationMonths,
|
||||||
|
tier : data.subTier,
|
||||||
|
total : data.cumlativeTotal
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const classes = 'sub';
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'twitch', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function twitchGiftSubsMessage(data) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
user : {
|
||||||
|
id : userID,
|
||||||
|
name : userName
|
||||||
|
}
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const messageID = createRandomString(40);
|
||||||
|
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
getTwitchAvatar(userID),
|
||||||
|
currentLang.twitch.giftedbomb({ count : data.total, tier : data.sub_tier, total : data.cumulative_total })
|
||||||
|
]);
|
||||||
|
|
||||||
|
const classes = 'sub';
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'twitch', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function twitchRaidMessage(data) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
from_broadcaster_user_login: userID,
|
||||||
|
from_broadcaster_user_name: userName
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const messageID = createRandomString(40);
|
||||||
|
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
getTwitchAvatar(userID),
|
||||||
|
currentLang.twitch.raid({ viewers : data.viewers })
|
||||||
|
]);
|
||||||
|
|
||||||
|
const classes = 'raid';
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventToChat(userID, messageID, 'twitch', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function twitchUpdateStatistics(data) {
|
||||||
|
const viewers = DOMPurify.sanitize(data.viewers);
|
||||||
|
document.querySelector('#statistics #twitch .viewers span').textContent = formatNumber(viewers);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function getTwitchEmotes(data) {
|
||||||
|
const message = data.message.message;
|
||||||
|
const emotes = data.emotes;
|
||||||
|
|
||||||
|
const words = message.split(" ");
|
||||||
|
emotes.sort((a, b) => b.startIndex - a.startIndex);
|
||||||
|
|
||||||
|
for (let i = 0; i < words.length; i++) {
|
||||||
|
emotes.forEach(emote => {
|
||||||
|
if (words[i] === emote.name) {
|
||||||
|
words[i] = `<img src="${emote.imageUrl}" data-emote-id="${emote.id}" alt="${emote.name}" class="emote">`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return words.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getTwitchAnnouncementEmotes(data) {
|
||||||
|
const message = data.text;
|
||||||
|
const emotes = data.parts;
|
||||||
|
|
||||||
|
const words = message.split(" ");
|
||||||
|
emotes.sort((a, b) => b.startIndex - a.startIndex);
|
||||||
|
|
||||||
|
for (let i = 0; i < words.length; i++) {
|
||||||
|
emotes.forEach(emote => {
|
||||||
|
if (words[i] === emote.text) {
|
||||||
|
words[i] = `<img src="${emote.imageUrl}" alt="${emote.text}" class="emote">`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return words.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getTwitchBadges(data) {
|
||||||
|
const badges = data.message.badges;
|
||||||
|
var htmlBadges = '';
|
||||||
|
badges.forEach((badge) => {
|
||||||
|
htmlBadges += `<img src="${badge.imageUrl}" class="badge">`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return htmlBadges;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getTwitchAvatar(user) {
|
||||||
|
if (avatars.has(user)) {
|
||||||
|
console.debug(`Avatar found for ${user}!`);
|
||||||
|
return avatars.get(user);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.debug(`Avatar not found for ${user}! Getting it from DECAPI!`);
|
||||||
|
var decapi = await fetch('https://decapi.me/twitch/avatar/' + user);
|
||||||
|
var newavatar = await decapi.text()
|
||||||
|
avatars.set(user, newavatar);
|
||||||
|
return newavatar;
|
||||||
|
}
|
||||||
|
}
|
344
js/youtube/module.js
Normal file
344
js/youtube/module.js
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
const showYouTubeMessages = getURLParam("showYouTubeMessages", true);
|
||||||
|
const showYouTubeSuperChats = getURLParam("showYouTubeSuperChats", true);
|
||||||
|
const showYouTubeSuperStickers = getURLParam("showYouTubeSuperStickers", false);
|
||||||
|
const showYouTubeMemberships = getURLParam("showYouTubeMemberships", true);
|
||||||
|
const showYouTubeGiftMemberships = getURLParam("showYouTubeGiftMemberships", true);
|
||||||
|
const showYouTubeMembershipsTrain = getURLParam("showYouTubeMembershipsTrain", true);
|
||||||
|
const showYouTubeStatistics = getURLParam("showYouTubeStatistics", true);
|
||||||
|
|
||||||
|
userColors.set('youtube', new Map());
|
||||||
|
|
||||||
|
if (showYouTubeStatistics == false) { document.querySelector('#statistics #youtube').style.display = 'none'; }
|
||||||
|
|
||||||
|
const youtubeMessageHandlers = {
|
||||||
|
'YouTube.Message': (response) => {
|
||||||
|
console.debug('YouTube Chat', response.data);
|
||||||
|
if (showYouTubeMessages == false)
|
||||||
|
return;
|
||||||
|
if (ignoreUserList.includes(response.data.user.name.toLowerCase()))
|
||||||
|
return;
|
||||||
|
youTubeChatMessage(response.data);
|
||||||
|
},
|
||||||
|
/*'YouTube.SuperChat': (response) => {
|
||||||
|
console.debug('YouTube SuperChat', response.data);
|
||||||
|
if (showYouTubeSuperChats == false) return;
|
||||||
|
youTubeSuperChatMessage(response.data);
|
||||||
|
},
|
||||||
|
'YouTube.SuperSticker': (response) => {
|
||||||
|
console.debug('YouTube SuperSticker', response.data);
|
||||||
|
if (showYouTubeSuperStickers == false) return;
|
||||||
|
youTubeSuperStickerMessage(response.data);
|
||||||
|
},
|
||||||
|
'YouTube.NewSponsor': (response) => {
|
||||||
|
console.debug('YouTube New Member', response.data);
|
||||||
|
if (showYouTubeMemberships == false) return;
|
||||||
|
youTubeNewSponsorMessage(response.data);
|
||||||
|
},
|
||||||
|
'YouTube.MemberMileStone': (response) => {
|
||||||
|
console.debug('YouTube Member Milestone', response.data);
|
||||||
|
if (showYouTubeMemberships == false) return;
|
||||||
|
youTubeNewSponsorMessage(response.data);
|
||||||
|
},
|
||||||
|
'YouTube.MembershipGift': (response) => {
|
||||||
|
console.debug('YouTube Gifted Membership', response.data);
|
||||||
|
if (showYouTubeGiftMemberships == false) return;
|
||||||
|
youTubeGiftedMembersMessage(response.data);
|
||||||
|
},
|
||||||
|
'YouTube.GiftMembershipReceived': (response) => {
|
||||||
|
console.debug('YouTube Gifted Membership Bomb', response.data);
|
||||||
|
if (showYouTubeMembershipsTrain == false) return;
|
||||||
|
youTubeGiftedMembersMessage(response.data);
|
||||||
|
},*/
|
||||||
|
'YouTube.StatisticsUpdated': (response) => {
|
||||||
|
console.debug(response.data);
|
||||||
|
if (showYouTubeStatistics == false) return;
|
||||||
|
YouTubeGiftReceivedMessage(response.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
for (const [event, handler] of Object.entries(youtubeMessageHandlers)) {
|
||||||
|
streamerBotClient.on(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
streamerBotClient.on('General.Custom', (response) => {
|
||||||
|
if (response.data.platform === 'YouTube') {
|
||||||
|
|
||||||
|
let json = response.data;
|
||||||
|
let ytdata = response.data.data;
|
||||||
|
|
||||||
|
switch (json.data.eventname) {
|
||||||
|
|
||||||
|
case 'Super Chat' :
|
||||||
|
console.debug('YouTube Super Chat', ytdata);
|
||||||
|
if (showYouTubeSuperChats == false) return;
|
||||||
|
youTubeSuperChatMessage(ytdata);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Super Sticker' :
|
||||||
|
console.debug('YouTube Super Sticker', ytdata);
|
||||||
|
if (showYouTubeSuperStickers == false) return;
|
||||||
|
youTubeSuperStickerMessage(ytdata);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'New Sponsor' :
|
||||||
|
console.debug('YouTube New Member', ytdata);
|
||||||
|
if (showYouTubeMemberships == false) return;
|
||||||
|
youTubeNewSponsorMessage(ytdata);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Member Milestone' :
|
||||||
|
console.debug('YouTube Member Milestone', ytdata);
|
||||||
|
if (showYouTubeMemberships == false) return;
|
||||||
|
youTubeNewSponsorMessage(ytdata);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Membership Gift' :
|
||||||
|
console.debug('YouTube Membership Gift', ytdata);
|
||||||
|
if (showYouTubeGiftMemberships == false) return;
|
||||||
|
youTubeGiftedMembersMessage(ytdata);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Gift Membership Received' :
|
||||||
|
console.debug('YouTube Gift Bomb Membership', ytdata);
|
||||||
|
if (showYouTubeMembershipsTrain == false) return;
|
||||||
|
YouTubeGiftReceivedMessage(ytdata);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.debug('General YouTube Data', ytdata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function youTubeChatMessage(data) {
|
||||||
|
if (data.message.startsWith("!") && excludeCommands == true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const {
|
||||||
|
user: {
|
||||||
|
id: userID,
|
||||||
|
profileImageUrl: avatar,
|
||||||
|
name: userName,
|
||||||
|
isVerified,
|
||||||
|
isSponsor,
|
||||||
|
isModerator,
|
||||||
|
isOwner,
|
||||||
|
},
|
||||||
|
eventId: messageID,
|
||||||
|
message,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const badgesHTML = [
|
||||||
|
isVerified && '<i class="fa-solid fa-check"></i>',
|
||||||
|
isSponsor && '<i class="fa-solid fa-star"></i>',
|
||||||
|
isModerator && '<i class="fa-solid fa-wrench"></i>',
|
||||||
|
isOwner && '<i class="fa-solid fa-video"></i>',
|
||||||
|
].filter(Boolean).join('');
|
||||||
|
|
||||||
|
const classes = [
|
||||||
|
isSponsor && 'sub',
|
||||||
|
isModerator && 'mod',
|
||||||
|
isOwner && 'owner',
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
classes: classes.join(' '),
|
||||||
|
avatar,
|
||||||
|
badges: badgesHTML,
|
||||||
|
userName,
|
||||||
|
color: await createRandomColor('youtube', userID),
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
};
|
||||||
|
addMessageToChat(userID, messageID, 'youtube', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function youTubeSuperChatMessage(data) {
|
||||||
|
const {
|
||||||
|
user: {
|
||||||
|
id: userID,
|
||||||
|
name: userName,
|
||||||
|
},
|
||||||
|
eventId: messageID,
|
||||||
|
currencyCode: currency,
|
||||||
|
amount,
|
||||||
|
message : textmessage
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
/*var moneycurrency = currency || 'USD';
|
||||||
|
var money = formatCurrency(amount, moneycurrency);*/
|
||||||
|
|
||||||
|
var money = amount;
|
||||||
|
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
``,
|
||||||
|
currentLang.youtube.superchat({
|
||||||
|
money : money,
|
||||||
|
message : textmessage
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const classes = 'superchat';
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
}
|
||||||
|
addEventToChat(userID, messageID, 'youtube', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function youTubeSuperStickerMessage(data) {
|
||||||
|
const {
|
||||||
|
user: {
|
||||||
|
id: userID,
|
||||||
|
name: userName,
|
||||||
|
},
|
||||||
|
eventId: messageID,
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
message : textmessage
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
/*var moneycurrency = currency || 'USD';
|
||||||
|
var money = formatCurrency(amount, moneycurrency);*/
|
||||||
|
|
||||||
|
var money = amount;
|
||||||
|
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
``,
|
||||||
|
currentLang.youtube.superchat({
|
||||||
|
money : money,
|
||||||
|
message : textmessage
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
const classes = 'supersticker';
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
}
|
||||||
|
addEventToChat(userID, messageID, 'youtube', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function youTubeNewSponsorMessage(data) {
|
||||||
|
const {
|
||||||
|
user: {
|
||||||
|
id: userID,
|
||||||
|
name: userName,
|
||||||
|
},
|
||||||
|
eventId: messageID,
|
||||||
|
levelName,
|
||||||
|
months,
|
||||||
|
tier,
|
||||||
|
} = data;
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
``,
|
||||||
|
currentLang.youtube.member({
|
||||||
|
months : months,
|
||||||
|
tier : levelName,
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
const classes = 'member';
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
}
|
||||||
|
addEventToChat(userID, messageID, 'youtube', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function youTubeGiftedMembersMessage(data) {
|
||||||
|
const {
|
||||||
|
user: {
|
||||||
|
id: userID,
|
||||||
|
name: userName,
|
||||||
|
},
|
||||||
|
eventId: messageID,
|
||||||
|
tier,
|
||||||
|
count
|
||||||
|
} = data;
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
``,
|
||||||
|
currentLang.youtube.giftedmembers({
|
||||||
|
total : count,
|
||||||
|
tier : tier
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
const classes = 'giftedmembers';
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
}
|
||||||
|
addEventToChat(userID, messageID, 'youtube', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function YouTubeGiftReceivedMessage(data) {
|
||||||
|
const {
|
||||||
|
user: {
|
||||||
|
id: userID,
|
||||||
|
name: userName,
|
||||||
|
},
|
||||||
|
gifter: {
|
||||||
|
id : gifterUserId,
|
||||||
|
name: gifterUserName
|
||||||
|
},
|
||||||
|
eventId: messageID,
|
||||||
|
tier
|
||||||
|
} = data;
|
||||||
|
const [avatar, message] = await Promise.all([
|
||||||
|
``,
|
||||||
|
currentLang.youtube.giftedmembers({
|
||||||
|
gifted : gifterUserName,
|
||||||
|
tier : tier
|
||||||
|
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
const classes = 'giftedtrainmembers';
|
||||||
|
const messageData = {
|
||||||
|
classes: classes,
|
||||||
|
avatar,
|
||||||
|
badges: '',
|
||||||
|
userName,
|
||||||
|
color: '#FFF',
|
||||||
|
message,
|
||||||
|
reply: '',
|
||||||
|
}
|
||||||
|
addEventToChat(userID, messageID, 'youtube', messageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function youTubeUpdateStatistics(data) {
|
||||||
|
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);
|
||||||
|
}
|
Reference in New Issue
Block a user