71 Commits

Author SHA1 Message Date
wompmacho
ce2e4541da Merge pull request #23 from wompmacho/add-stream-overlay-option
unable to Add stream overlay option, alternatives and fixes, also rename project
2020-11-13 05:17:27 -05:00
wompmacho
1826dce523 not happening
change name - wompchat
overlay attempt was made. turns out cant link obs to chrome extension web page. added greenscreen option instead. best i can do until chat bot is made. need a web page hosted that can be manipulated and can be linked into obs.
some other unrelated fixes - minor bugs
added escape key for popup
expanded pupup window to full width of chat
2020-11-13 05:16:24 -05:00
wompmacho
fb13e2f056 creating messanger for content scripst
to pass messages to overlay
2020-11-12 06:05:32 -05:00
wompmacho
43c25e103e Merge pull request #22 from wompmacho/updatelinks
Update README.md
2020-11-11 16:59:46 -05:00
wompmacho
cfd75d69ed Update README.md
update links
2020-11-11 16:59:36 -05:00
wompmacho
c8fda4aac9 Merge pull request #21 from wompmacho/cleanup
Delete full512.png
2020-11-11 16:45:31 -05:00
wompmacho
badd13acb1 Delete full512.png 2020-11-11 16:45:16 -05:00
wompmacho
c87e566cc2 Merge pull request #20 from wompmacho/cleanup
Cleanup
2020-11-11 16:42:55 -05:00
wompmacho
67d9446202 cleanep 2020-11-11 16:41:21 -05:00
wompmacho
07c4a238c9 cleanup 2020-11-11 16:41:06 -05:00
wompmacho
a67d057764 Merge pull request #19 from wompmacho/links-for-cdns-being-dumb
Links for cdns being dumb
2020-11-11 16:36:53 -05:00
wompmacho
8ded4f44ed Merge branch 'main' into links-for-cdns-being-dumb 2020-11-11 16:35:18 -05:00
wompmacho
85f4f15b01 redirct for website 2020-11-11 16:27:53 -05:00
wompmacho
d2bbbc277e redirct for website 2020-11-11 16:25:54 -05:00
wompmacho
f470c61558 moving media to gcloud, fix for alternateLineColors, rebrand to WompChat 2020-11-11 16:16:45 -05:00
wompmacho
64ab22c205 Merge pull request #16 from wompmacho/dev
Dev
2020-11-11 08:33:42 -05:00
wompmacho
cacfca6b9b Merge branch 'main' into dev 2020-11-11 08:33:32 -05:00
wompmacho
51f6a1fc59 Create .jshintrc 2020-11-11 08:32:10 -05:00
wompmacho
dd2c56a5dc added psd to git ignore, updating names 2020-11-09 20:18:16 -05:00
wompmacho
8cfd1a37f2 updated local icons 2020-11-09 20:14:47 -05:00
wompmacho
ba26b46004 new icon/updated name 2020-11-09 20:12:34 -05:00
wompmacho
0bd6fac0d9 Merge pull request #15 from wompmacho/links-for-cdns-being-dumb
Update README.md
2020-11-09 16:01:31 -05:00
wompmacho
9963fa96a2 Update README.md
updated links. github cdn being dumb
2020-11-09 16:01:20 -05:00
wompmacho
3d995c5564 Merge pull request #14 from wompmacho/dev
Dev
2020-11-04 19:16:17 -05:00
wompmacho
d5e8d51f93 removed un nec emotes 2020-11-04 19:15:18 -05:00
wompmacho
ffb55757b9 Update README.md
updated link for cdn
2020-11-04 19:13:22 -05:00
wompmacho
7ac05b2686 moved imgs to cdn 2020-11-04 19:11:15 -05:00
wompmacho
1ac2703e52 Update New Name and version 2020-11-04 16:30:30 -05:00
wompmacho
55b8ffd9c5 update cdn links 2020-11-04 16:18:11 -05:00
wompmacho
3a7827649e Update README.md
update with cdn link
2020-11-04 16:05:50 -05:00
wompmacho
f35c3c3ad1 Merge pull request #13 from wompmacho/dev
Dev
2020-11-04 16:02:11 -05:00
wompmacho
81bc8481b3 updating links for cdn 2020-11-04 16:01:45 -05:00
wompmacho
ee1c9aed99 removing uness assets 2020-11-04 15:48:09 -05:00
wompmacho
457ddfdce6 moving assets to github cdn, shrinking file size 2020-11-04 15:47:28 -05:00
wompmacho
3f00a4c046 added shields to welcome page 2020-11-03 15:46:02 -05:00
wompmacho
c8bc9241d2 Update README.md
change spacing between buttons
2020-11-03 01:08:50 -05:00
wompmacho
f87efcc978 Merge pull request #12 from wompmacho/adding-btc-badge
Adding btc badge
2020-11-03 01:07:21 -05:00
wompmacho
c964607672 Update README.md
updated url to btc qrcode
2020-11-03 01:06:48 -05:00
wompmacho
17b8d52d6c Create btc.JPG
adding btc qr code
2020-11-03 01:05:00 -05:00
wompmacho
e704136ebd Update README.md
added btc shield
2020-11-03 01:01:36 -05:00
wompmacho
e1f96f7647 Update README.md
updated markup for correct url for discord
2020-11-02 23:37:51 -05:00
wompmacho
066a98ea00 Update README.md
added discord to readme
2020-11-02 23:34:47 -05:00
wompmacho
c6eab86651 Merge pull request #11 from wompmacho/add-license-1
Create LICENSE.md
2020-11-02 02:57:03 -05:00
wompmacho
54a8939e9f Create LICENSE.md
added MIT LICENSE
2020-11-02 02:56:46 -05:00
wompmacho
1e7d39071c Merge pull request #10 from wompmacho/dev
removed unnecessary license comments
2020-11-02 02:55:08 -05:00
wompmacho
daaf8714ae removed unnecesary license comments
removed unnecesary license comments generated by that add-license thing
2020-11-02 02:54:46 -05:00
wompmacho
f9b3f40d1e Update README.md
size change
2020-11-02 00:58:07 -05:00
wompmacho
3d6743adf8 Update README.md 2020-11-02 00:53:53 -05:00
wompmacho
76e3da6021 Update README.md
img in p wrapper center
2020-11-02 00:53:12 -05:00
wompmacho
8f41b04cb7 Update README.md
test
2020-11-02 00:50:52 -05:00
wompmacho
69b8b10a76 Update README.md
get rid of format section
2020-11-02 00:48:10 -05:00
wompmacho
44a60f5227 Update README.md
attempt to ad image/gif
2020-11-02 00:47:08 -05:00
wompmacho
6f319ba942 Merge pull request #9 from wompmacho/dev
removed redundant files
2020-11-02 00:39:21 -05:00
wompmacho
9618890ed6 removed redundant files 2020-11-02 00:39:00 -05:00
wompmacho
1c94607a57 Merge pull request #8 from wompmacho/dev
Update manifest.json
2020-11-02 00:26:52 -05:00
wompmacho
c9d3a3e2f2 Update manifest.json
version update
2020-11-02 00:26:34 -05:00
wompmacho
5679a6fc4a Merge pull request #7 from wompmacho/dev
removed redundant files
2020-11-02 00:03:25 -05:00
wompmacho
eb93a16a53 removed redundant files 2020-11-02 00:02:50 -05:00
wompmacho
b4710c00e4 Merge pull request #6 from wompmacho/dev
Dev
2020-11-01 23:48:19 -05:00
wompmacho
cf52afc0ba moving welcome banner change to index
remove extra lines
moved welcome banner to index
moved set defaults inside main of index
2020-11-01 23:35:22 -05:00
wompmacho
5661778850 Update Message.js
removed extra lines
2020-11-01 23:11:34 -05:00
wompmacho
b2320ff2dc Merge pull request #5 from wompmacho/dev
added build to gitignore, removing build folder as cleanup
2020-11-01 22:56:52 -05:00
wompmacho
30db96fc19 added build to gitignore, removing build folder as cleanup 2020-11-01 22:56:29 -05:00
wompmacho
edfae9fc69 Merge pull request #4 from wompmacho/dev
Update .gitignore
2020-11-01 22:50:46 -05:00
wompmacho
123bbd61dd Update .gitignore 2020-11-01 22:47:56 -05:00
wompmacho
12f0a7fdec Merge pull request #3 from wompmacho/dev
Dev
2020-11-01 22:16:01 -05:00
wompmacho
23f2612ecb Merge branch 'main' into dev 2020-11-01 22:15:33 -05:00
wompmacho
fe421495fc new version and added github 2020-11-01 21:58:22 -05:00
wompmacho
8a9661f2c9 Merge pull request #2 from wompmacho/dev
Dev
2020-11-01 21:07:45 -05:00
wompmacho
3d81e92ecb adding dev files to master
adding all the dev files, package, webpack etc, including build which will contain minified release
2020-11-01 21:02:00 -05:00
wompmacho
f47b10a05e Create .gitignore
added ignored folders for git
2020-11-01 20:57:10 -05:00
56 changed files with 66440 additions and 26394 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
dev-build
node_modules
build
*.psd
*.ai
cdn/
artwork/
.jshintrc

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 wompmacho
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,2 +1,8 @@
# live-chat <p align="center">
<img width="443" height="592" src="https://storage.googleapis.com/womp-website_cloudbuild/cdn/wompchat/gif/emotesMenu.gif">
</p>
# wompchat
[![Discord](https://img.shields.io/discord/238458588169895937?label=Discord&style=plastic)](https://discord.gg/pVNnTDA) [![BTC](https://img.shields.io/static/v1?label=BTC&style=plastic&message=3QkKHVyT1nZLEeH8f77bUhXorXKPQs5gzN&color=red)](https://storage.googleapis.com/womp-website_cloudbuild/cdn/wompchat/img/btc.JPG)
A Chrome Extension To Improve YouTube Livestream Chat A Chrome Extension To Improve YouTube Livestream Chat

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

File diff suppressed because one or more lines are too long

2809
content.js

File diff suppressed because one or more lines are too long

View File

@@ -1,92 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Welcome Page</title>
</head>
<body>
<div class="success-overlay">
<div class="icon-container">
<i class="material-icons">done</i>
</div>
</div>
<div class="container">
<div class="logo">
<img src="../assets/icons/icon512.png" alt="Live Chat Logo">
</div>
<h1><span class="fw-light">Welcome to the </span><span class="fw-bold">Chat</span></h1>
<p class="heading-note">This is still very beta</p>
<hr>
<div class="links_div">
<a class="links_bottom" href="https://discord.gg/pVNnTDA" title="Join our Discord Community">Join our Discord Community</a>
<a class="links_bottom" href="https://streamelements.com/wompmacho-5882/tip" title="Donations">Donations</a>
<a class="links_bottom" href="https://www.youtube.com/wompmacho" title="Youtube">Youtube</a>
<a class="links_bottom" href="mailto:wompmacho@gmail.com">Contact Me</a>
</div>
<hr>
<h2>
Soon to come:
</h2>
<ul>
<li>Specific channel Emotes</li>
<li>Search for Emote Panel</li>
<li>Autocomplete for emote Selection</li>
<li>Theater Mode Fix for that quality Stream and Chat time</li>
<li>Moderation Options Popup</li>
<li>User Profile Info Popup</li>
<li>Other Styling Options</li>
</ul>
<hr>
<h2>How To:</h2>
<p>
There is still a lot of stuff I am working on. This is very Beta at the moment so
</p>
<h2>Use At Your Own Risk</h2>
<p>
This is a Chrome Extension for Youtube Live Streams, adding some Quality of Life improvements for the Chat.
Adds Top, Trending and Global (500ish) Emotes from popular sites. These Update with what is Trending.
</p>
<p>
Don't forget to pin this extension for easy access.
</p>
<img src="../assets/gif/pinYourExtension.gif" alt="pinYourExtension.gif">
<p>
Youtube Live is slow to load its pages currently.
Give the extension a moment while the page's iframes are loading.
Once you see the Emote Panel Icon you can open the Emote Selection Panel.
</p>
<img src="../assets/gif/emotesMenu.gif" alt="pinYourExtension.gif">
<p>
Over around 500 Top and Trending Emotes are loaded from popular sites.
You can Enable/Disable them in the Options Menu.
</p>
<img src="../assets/gif/optionsMenu.gif" alt="pinYourExtension.gif">
<p>
Can Also Click on UserNames To Autofill an @ Notification
</p>
<img src="../assets/gif/clickUserNamesToAtThem.gif" alt="pinYourExtension.gif">
<h2>Use At Your Own Risk</h2>
<h1>Enjoy!</h1>
</div>
<script src="../welcomePage.js"></script>
</body>
</html>

20531
options.js

File diff suppressed because one or more lines are too long

4695
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "WompChat",
"version": "1.0.3",
"description": "Enhances the YouTube Live Streaming experience with Emotes, Custom Styling and quality of life improvements.",
"scripts": {
"start": "webpack --progress --watch",
"watch": "npm start",
"build": "webpack --progress",
"prod": "webpack --progress --config webpack.prod.js -p"
},
"author": "wompmacho",
"repository": {
"type": "git",
"url": "git://github.com/wompmacho/live-chat.git"
},
"dependencies": {
"lodash": "^4.17.15",
"preact": "^8.2.7",
"uglifyjs-webpack-plugin": "^1.3.0"
},
"devDependencies": {
"copy-webpack-plugin": "^4.5.2",
"css-loader": "^1.0.0",
"style-loader": "^0.22.1",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"webpack": "^4.17.1",
"webpack-cli": "^3.1.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 67 KiB

38
src/background/Setup.js Normal file
View File

@@ -0,0 +1,38 @@
import PersistentSyncStorage from '../helpers/PersistentSyncStorage';
import CONFIG from '../config';
const ensure = () => {
return new Promise((res, rej) => {
// Resolves if setup is complete
if(PersistentSyncStorage.data.setupComplete) {
// Ensure new options (on extension update) are added to options object
PersistentSyncStorage.set({
options: Object.assign({}, CONFIG.defaultOptions, PersistentSyncStorage.data.options)
});
return res();
}
// Otherwise inits setup
const onSetupComplete = (request, sender, sendResponse) => {
if(request.name === 'setupComplete') {
chrome.runtime.onMessage.removeListener(onSetupComplete);
PersistentSyncStorage.set({
setupComplete: true
});
res();
}
return true;
};
chrome.tabs.create({ url: 'html/welcome.html' });
chrome.runtime.onMessage.addListener(onSetupComplete);
console.log('Storage Setup Complete, Linking User To Welcome Page.');
});
};
export default {ensure};

25
src/background/index.js Normal file
View File

@@ -0,0 +1,25 @@
import PersistentSyncStorage from 'src/helpers/PersistentSyncStorage';
import Setup from './Setup';
import CONFIG from 'src/config';
class Main {
constructor() {
this.init = this.init.bind(this);
PersistentSyncStorage.on('ready', () => {
this.setupOptions();
Setup.ensure().then(this.init);
});
}
init() {}
setupOptions() {
// Ensure options store is setup
if(!PersistentSyncStorage.has('options')) {
PersistentSyncStorage.set({ options: CONFIG.defaultOptions });
}
}
}
const main = new Main();

50
src/chat_overlay.js Normal file
View File

@@ -0,0 +1,50 @@
import './stylus/content.styl';
import './stylus/chat_overlay.styl';
const message_container = document.getElementById("message_container");
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if(sender.origin == "https://www.youtube.com"){
if (request.message == "preloadMessages"){
message_container.prepend(convertToHTMLElm(request.node));
message_container.scrollIntoView({block: 'end'});
}
if (request.message == "newMessage"){
message_container.append(convertToHTMLElm(request.node));
message_container.scrollIntoView({block: 'end'});
}
if (request.message == "removeMessage"){
destroyMessage(convertToHTMLElm(request.node));
}
}
}
);
function convertToHTMLElm(node){
//var node = new DOMParser().parseFromString(node, 'text/html').documentElement;
var temp = document.createElement('div');
temp.innerHTML = node;
node = temp.firstChild;
return node;
}
function destroyMessage(node){
const messageId = node.getAttribute('message-id');
console.log(messageId);
console.log(message);
const message = document.querySelectorAll(`[message-id=${messageId}]`);
if(message != undefined){
message.destroy();
}
}

28
src/config.js Normal file
View File

@@ -0,0 +1,28 @@
const CONFIG = {
defaultOptions: {
// Emote Options
enableBTTVEmotes: true,
enableFrankerEmotes: true,
enableTwitchEmotes: true,
// Chat Options
kappaFix: true,
theaterModeFix: false,
setAuthorColor: false,
showTimeStamp: false,
alternateLineColor: false,
hideAuthorIcons: false,
hideWelcomBanner: false,
setTwitchColors: false,
setLiveChat: true,
allowTextSlider: false,
setGreenScreen: false,
// Var
textSizeSlider: 'inherit',
}
};
export default CONFIG;

View File

@@ -0,0 +1,48 @@
class ChatScroller {
constructor() {
this.scroll = this.scroll.bind(this);
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.scroller = null;
this.interval = null;
}
init() {
this.getScroller()
.then(() => {
//this.scroller.addEventListener('mouseleave', this.start);
this.scroller.addEventListener('mouseenter', this.stop);
this.start();
});
}
start() {
this.interval = setInterval(
this.scroll,
250
);
}
stop() {
clearInterval(this.interval);
}
scroll() {
this.scroller.scrollIntoView({block: 'end'});
}
getScroller() {
const checkForScroller = (res, rej) => {
this.scroller = document.getElementById('item-scroller');
if(this.scroller !== null) {
res();
} else {
setTimeout(checkForScroller.bind(this, res, rej), 250);
}
};
return new Promise(checkForScroller);
}
}
export default ChatScroller;

238
src/content/ChatWatcher.js Normal file
View File

@@ -0,0 +1,238 @@
import Emotes from './Emotes';
import Message from './Message';
import PersistentSyncStorage from 'src/helpers/PersistentSyncStorage';
class ChatWatcher {
constructor() {
this.watchChat = this.watchChat.bind(this);
this._chatContainer = null;
this._observer = null;
this.messages = new Map();
}
init() {
return new Promise((res, rej) => {
this.getChatContainer().then(Emotes.init).then(() => {
this.addEmotePopup();
this.watchChat();
this.parsePreloadedMessages();
});
});
}
getChatContainer() {
// Parent of actual chat (children are messages)
const checkForContainer = (res, rej) => {
this._chatContainer = document.querySelector('#items.style-scope.yt-live-chat-item-list-renderer');
if(this._chatContainer !== null) {
res();
} else {
setTimeout(checkForContainer.bind(this, res, rej), 250);
}
};
return new Promise(checkForContainer);
}
parsePreloadedMessages() {
const messages = this._chatContainer.children;
for(let i = messages.length-1; i >= 0; i--) {
const node = messages[i];
if(this.isMessageNode(node)) {
const message = new Message(node);
// chrome.runtime.sendMessage({message: "preloadMessages", node: node.outerHTML}, function(response) {
// // dont need to do anything
// });
}
}
}
watchChat() {
console.log('Chat observer started');
this._observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
const { addedNodes, removedNodes } = mutation;
// Added nodes
if(typeof addedNodes !== 'undefined' && addedNodes.length > 0) {
for(let i = 0, length = addedNodes.length-1; i <= length; i++) {
const node = addedNodes[i];
if(this.isMessageNode(node)) {
this.onNewMessage(node);
// send message
// chrome.runtime.sendMessage({message: "newMessage", node: node.outerHTML}, function(response) {
// // dont need to do anything
// });
}
}
}
// Removed nodes
if(typeof removedNodes !== 'undefined' && removedNodes.length > 0) {
for(let i = 0, length = removedNodes.length-1; i <= length; i++) {
const node = removedNodes[i];
if(this.isMessageNode(node) && this.isObservedMessage(node)) {
this.onObservedMessageRemoved(node);
}
}
}
});
});
this._observer.observe(this._chatContainer, {
childList: true,
attributes: false,
characterData: false,
subtree: false
});
}
onNewMessage(node) {
const message = new Message(node);
// Don't store message if has 0 emotes
if(message.hasEmotes) {
this.messages.set(message.id, message);
}
}
onObservedMessageRemoved(node) {
const messageId = node.getAttribute('message-id');
const message = this.messages.get(messageId);
if(message != undefined){
message.destroy();
}
this.messages.delete(messageId);
}
isMessageNode(node) {
return node.tagName === 'YT-LIVE-CHAT-TEXT-MESSAGE-RENDERER';
}
isObservedMessage(node) {
return node.getAttribute('message-id') !== null;
}
///////////////////////////////////////////////////////////////////
addEmotePopup(){
// create emote button
const emoteButton = document.createElement('button');
emoteButton.classList.add('emoteButton');
emoteButton.textContent = '';
// append button to action-buttons list
const chatButtonSelectionList = document.getElementById('action-buttons');
chatButtonSelectionList.parentNode.insertBefore(emoteButton, chatButtonSelectionList);
// create popupDiv
const popUpDiv = document.createElement('div');
popUpDiv.classList.add('popup');
popUpDiv.classList.add('hideElement');
function emoteAppend(keysITer){
// create divider
var hr = document.createElement('hr');
hr.classList.add('emoteDivider');
for (let index = 0; index < keysITer.length; index++) {
const element = keysITer[index];
var emote_div = document.createElement('emote_div');
emote_div.innerHTML = (Emotes.get(element).html);
popUpDiv.appendChild(emote_div);
}
popUpDiv.appendChild(hr);
}
// create text
var bttv_text = document.createElement('h2');
bttv_text.textContent = "BTTV";
bttv_text.classList.add('emotePopUpText');
var franker_text = document.createElement('h2');
franker_text.textContent = "FrankerFacez";
franker_text.classList.add('emotePopUpText');
var twitch_text = document.createElement('h2');
twitch_text.textContent = "Twitch";
twitch_text.classList.add('emotePopUpText');
// need ittr to search each dict and append to dom
let keysITer = null;
keysITer = Array.from(Emotes.specialEmotesDictionary.keys());
emoteAppend(keysITer);
if(PersistentSyncStorage.data.options.enableBTTVEmotes){
popUpDiv.appendChild(bttv_text);
keysITer = Array.from(Emotes.bttv_Dictionary.keys());
emoteAppend(keysITer);
}
if(PersistentSyncStorage.data.options.enableFrankerEmotes){
popUpDiv.appendChild(franker_text);
keysITer = Array.from(Emotes.franker_Dictionary.keys());
emoteAppend(keysITer);
}
if(PersistentSyncStorage.data.options.enableTwitchEmotes){
popUpDiv.appendChild(twitch_text);
keysITer = Array.from(Emotes.twitch_Dictionary.keys());
emoteAppend(keysITer);
}
// add div to doc
chatButtonSelectionList.appendChild(popUpDiv);
// listen for popup button
emoteButton.addEventListener('click', function(){
popUpDiv.classList.toggle('hideElement');
console.log('emote popup button clicked');
});
// escape for popup div
document.onkeydown = function(evt) {
if (evt.key === "Escape" && !popUpDiv.classList.contains('hideElement')) {
popUpDiv.classList.toggle('hideElement');
return;
}
};
// get input area
var inputArea = document.querySelector('#input.yt-live-chat-text-input-field-renderer');
var inputAreaLabel = document.querySelector('#label.yt-live-chat-text-input-field-renderer');
// add alt tag to chat
function emoteToTextArea(){
inputArea.textContent += this.alt + " ";
inputArea.focus();
inputAreaLabel.textContent = "";
popUpDiv.classList.toggle('hideElement');
console.log(this.alt + " emote button selected");
}
// listener button for emotes
var EMOTICONS = document.getElementsByTagName('img');
for (let index = 0; index < EMOTICONS.length; index++) {
const element = EMOTICONS[index];
element.addEventListener('click', emoteToTextArea, false);
}
console.log((keysITer.length+1) + " Emotes Added");
}// end addEmotePopup
///////////////////////////////////////////////////////////////////
}// end chat watcher
export default ChatWatcher;

View File

@@ -0,0 +1,16 @@
class Emote {
constructor({ code, url }) {
this.code = code;
this.url = url;
}
get html() {
return (`
<span class="Emote">
<img title="${this.code}" src="${this.url}" alt="${this.code}">
</span>
`).trim();
}
}
export default Emote;

198
src/content/Emotes/index.js Normal file
View File

@@ -0,0 +1,198 @@
import PersistentSyncStorage from 'src/helpers/PersistentSyncStorage';
import Emote from './Emote';
class Emotes {
constructor() {
this.dictionary = new Map();
// identification for popup
this.twitch_Dictionary = new Map();
this.bttv_Dictionary = new Map();
this.franker_Dictionary = new Map();
this.specialEmotesDictionary = new Map();
this.init = this.init.bind(this);
}
init() {
return Promise.all([
(PersistentSyncStorage.data.options.enableBTTVEmotes && this.loadBTTVEmote()),
(PersistentSyncStorage.data.options.enableFrankerEmotes && this.loadFrankerEmotes()),
(PersistentSyncStorage.data.options.enableTwitchEmotes && this.loadTwitchEmotes()),
(this.specialEmotes())
]);
}
/////////////////////////////////////////////////////////////////////////////////
get(key) {
return this.dictionary.get(key);
}
set(key, value) {
return this.dictionary.set(key, new Emote(value));
}
has(key) {
return this.dictionary.has(key);
}
//////////////////////////////////////////////////////////////////
bbtv_ToDict(json){
for (let index = 0; index < json.length; index++) {
const { emote, total } = json[index];
const url = `https://cdn.betterttv.net/emote/${emote.id}/3x`;
this.dictionary.set(emote.code, new Emote({ code: emote.code, url }));
this.bttv_Dictionary.set(emote.code, new Emote({ code: emote.code, url }));
}
}
bbtv_cached_ToDict(json){
for (let index = 0; index < json.length; index++) {
const { id, code } = json[index];
const url = `https://cdn.betterttv.net/emote/${id}/3x`;
this.dictionary.set(code, new Emote({ code: code, url }));
this.bttv_Dictionary.set(code, new Emote({ code: code, url }));
}
}
// loadEmote is where we collect an object array of emotes from bttv api
async loadBTTVEmote(){
// top 100 emotes query = ?limit=100&offset=100
const bttv_top_api_url = "https://api.betterttv.net/3/emotes/shared/top?limit=100";
const bttv_top_api_response = await fetch(bttv_top_api_url);
var top_Json = await bttv_top_api_response.json();
// tredning emotes
const bttv_trending_api_url = "https://api.betterttv.net/3/emotes/shared/trending?limit=100";
const bttv_trending_api_response = await fetch(bttv_trending_api_url);
var trending_Json = await bttv_trending_api_response.json();
// global emotes are weird, stored in seperate cache and do not give all the normal attributes
const bttv_global_api_url = "https://api.betterttv.net/3/cached/emotes/global";
const bttv_global_api_response = await fetch(bttv_global_api_url);
var global_Json = await bttv_global_api_response.json();
this.bbtv_ToDict(top_Json);
this.bbtv_ToDict(trending_Json);
this.bbtv_cached_ToDict(global_Json);
}
////////////////////////////////////////////////////////////////
frankerToDict(json){
for (let index = 0; index < json.emoticons.length; index++) {
const { name, urls } = json.emoticons[index];
var url = "";
if(urls[4] != undefined){
url = urls[4];
}else if(urls[2] != undefined){
url = urls[2];
}else{
url = urls[1];
}
// fix for overlay, remove "//" at start of url
var protocol = "https:";
url = protocol.concat(url);
this.dictionary.set(name, new Emote({ code: name, url }));
this.franker_Dictionary.set(name, new Emote({ code: name, url }));
}
}
// loadFrankerEmotes is where we collect an object array of emotes from franker api
async loadFrankerEmotes(){
const franker_top_api_url = "https://api.frankerfacez.com/v1/emoticons?sort=count-desc";
const first50Response = await fetch(franker_top_api_url);
var first50json = await first50Response.json();
var next50Link = first50json._links.next;
const second50Response = await fetch(next50Link);
var second50json = await second50Response.json();
// Top 100
this.frankerToDict(first50json);
this.frankerToDict(second50json);
}
////////////////////////////////////////////////////////////////
twitchToDict(json){
for (let index = 0; index < json.emotes.length; index++) {
const { code, id } = json.emotes[index];
const url = `https://static-cdn.jtvnw.net/emoticons/v1/${id}/3.0`;
this.dictionary.set(code, new Emote({ code: code, url }));
this.twitch_Dictionary.set(code, new Emote({ code: code, url }));
}
}
// loadTwitchEmotes is where we collect an object array of emotes from twitch api
async loadTwitchEmotes(){
// https://api.twitchemotes.com/api/v4/channels/0 - twitch globals - 232 items
// https://static-cdn.jtvnw.net/emoticons/v1/25/1.0 - cdn
// Global
const twitch_global_api_url = "https://api.twitchemotes.com/api/v4/channels/0";
const twitch_global_api_response = await fetch(twitch_global_api_url);
var twitch_global_Json = await twitch_global_api_response.json();
this.twitchToDict(twitch_global_Json);
}
// ♥
specialEmotes(){
var emoteObj = {
"emotes": [
{
"code": "wompWTF",
"url": "https://static-cdn.jtvnw.net/emoticons/v1/301653066/3.0"
},
{
"code": "wompISeeYou",
"url": "https://static-cdn.jtvnw.net/emoticons/v1/301506153/3.0"
},
{
"code": "wompCry",
"url": "https://static-cdn.jtvnw.net/emoticons/v1/301506193/3.0"
},
{
"code": "BabyCorona",
"url": "https://static-cdn.jtvnw.net/emoticons/v1/301629296/3.0"
},
{
"code": "LEL",
"url": "https://static-cdn.jtvnw.net/emoticons/v1/431249/3.0"
}
]
};
for (let index = 0; index < emoteObj.emotes.length; index++) {
const element = emoteObj[index];
const { code, url } = emoteObj.emotes[index];
this.dictionary.set(code, new Emote({ code: code, url}));
this.specialEmotesDictionary.set(code, new Emote({ code: code, url}));
}
}
}// End Emotes
export default new Emotes;

208
src/content/Message.js Normal file
View File

@@ -0,0 +1,208 @@
import Emotes from './Emotes';
import PersistentSyncStorage from 'src/helpers/PersistentSyncStorage';
class Message {
constructor(messageNode) {
this.node = messageNode;
this.id = this.node.id; // this.id should not be used to reference the node, dom id changes due to optimisitc updates
this.hasEmotes = null;
this.observer = null;
this.parsedText = ''; // This should be fine since you can't edit/change messages
this.parseText();
this.setDefaultSelections();
if(this.hasEmotes) {
this.node.setAttribute('message-id', this.id);
this.setHtml();
this.watch();
}
}
get textNode() {
const node = this.node.querySelector('#message');
return {
node,
text: node.innerText
};
}
parseText() {
const rawWords = this.textNode.text.split(' ');
for(let i = 0, length = rawWords.length; i < length; i++) {
const word = this.parseIllegalCharcters(rawWords[i]);
const emote = Emotes.get(word);
//console.log(Emotes.get(word));
if(typeof emote === 'undefined') {
this.parsedText += word + ' ';
} else {
this.hasEmotes = true;
this.parsedText += emote.html + ' ';
}
}
}
watch() {
this.observer = new MutationObserver(mutations => {
let emoteRemoved = false;
mutations.forEach(mutation => {
if(typeof mutation.removedNodes === 'undefined') return;
if(mutation.removedNodes.length <= 0) return; // This must be after undefined check
for(let i = 0, length = mutation.removedNodes.length; i < length; i++) {
const removedNode = mutation.removedNodes[i];
if(typeof removedNode.className === 'string' && // check if className exists, is 'SVGAnimatedString' when window resized and removed
~removedNode.className.indexOf('Emote') !== 0) {
emoteRemoved = true;
}
}
});
if(emoteRemoved && document.body.contains(this.node)) {
this.setHtml();
}
});
this.observer.observe(this.node, {
childList: true,
attributes: false,
characterData: false,
subtree: true
});
}
setHtml() {
this.textNode.node.innerHTML = this.parsedText;
}
parseIllegalCharcters(word) {
//  === 'ZERO WIDTH NO-BREAK SPACE'
return word.replace('', '').trim();
}
destroy() {
if(this.observer !== null) {
this.observer.disconnect();
this.observer = null;
}
}
///////////////////////////////////////////////////////////////////
// Setting Options for Each Message
setDefaultSelections(){
///////////////////////////////////////////////////////////////////
// Checks for kappa and replaces emoji element with kappa
if(PersistentSyncStorage.data.options.kappaFix) {
var stupidKappa = document.querySelectorAll('#message.yt-live-chat-text-message-renderer .emoji.yt-live-chat-text-message-renderer');
for (let index = 0; index < stupidKappa.length; index++) {
const stupidElement = stupidKappa[index];
var stupidToolTip = stupidElement.getAttribute('shared-tooltip-text');
if(stupidToolTip == ':full_moon_face:'){
const newSpan = document.createElement('span');
newSpan.classList.add('Emote');
newSpan.innerHTML = '<img src="https://static-cdn.jtvnw.net/emoticons/v1/25/3.0" alt="kappa">';
stupidElement.parentNode.replaceChild(newSpan, stupidElement);
}
}
}
///////////////////////////////////////////////////////////////////
//Set Author Colors
if(PersistentSyncStorage.data.options.setAuthorColor && this.node.getAttribute('author-type') !== 'owner') {
this.setAuthorColor();
}
///////////////////////////////////////////////////////////////////
// Author Icons
var author_photo = this.node.querySelector('#author-photo');
// Set Hide Author Icons
if (PersistentSyncStorage.data.options.hideAuthorIcons) {
author_photo.classList.add("hideElement");
}
// TimeStamp
var timestamp = this.node.querySelector('#timestamp');
// Set Show TimeStamp
if (PersistentSyncStorage.data.options.showTimeStamp) {
this.node.classList.add("showTimeStamp");
}
///////////////////////////////////////////////////////////////////
// Author Name @ auto paste in text area
this.node.querySelector('#author-name').addEventListener("click", function(){
var inputArea = document.querySelector('#input.yt-live-chat-text-input-field-renderer');
var inputAreaLabel = document.querySelector('#label.yt-live-chat-text-input-field-renderer');
inputArea.innerText = "@" + this.innerText;
const textLength = inputArea.innerText.length;
inputArea.focus();
inputAreaLabel.innerText = "";
});
///////////////////////////////////////////////////////////////////
// Set Font Size
var textSizeSlider = PersistentSyncStorage.data.options.textSizeSlider;
if (PersistentSyncStorage.data.options.allowTextSlider) {
this.node.setAttribute('style', 'font-size:' + textSizeSlider + 'px' + '!important');
this.node.classList.add("AuthorFix");
}
///////////////////////////////////////////////////////////////////
// Set Twitch Styling
if (PersistentSyncStorage.data.options.setTwitchColors) {
this.node.classList.add("setTwitchColors");
author_photo.classList.add("hideElement");
}
///////////////////////////////////////////////////////////////////
}// end setDefaultSelections
setAuthorColor() {
let imageSrc = null;
if(this.node.hasChildNodes && this.node.contains(this.node.querySelector('#author-photo'))){
if(this.node.querySelector('#author-photo').querySelector('img').src != null){
imageSrc = this.node.querySelector('#author-photo').querySelector('img').src;
const idRegexp = /\/-([A-Za-z-_\d])/;
try {
if(idRegexp.exec(imageSrc) !== null){
const parsedSRC = idRegexp.exec(imageSrc)[1];
this.node.classList.add(`chat-color-${parsedSRC}`);
}
} catch (error) {
// for some reason nodes from user img.src are getting weird link on occasion
console.log(error);
console.log(imageSrc);
}
}
}
}
///////////////////////////////////////////////////////////////////
}// end Message
export default Message;

View File

@@ -0,0 +1,39 @@
import EventEmitter from 'events';
class RouteWatcher extends EventEmitter {
constructor() {
super();
this.target = document.querySelector('head > title');
this.observer = null;
this.init();
}
init() {
this.observer = new MutationObserver(mutations => {
mutations.forEach((m) => {
/**
* Title is set to 'YouTube Gaming' on main routes
* and between routes.
*/
if(m.target.innerText === 'YouTube Gaming') {
this.emit('main');
} else {
this.emit('change');
}
});
});
if(this.target !== null) { // Popout chat does not have title tag
this.observer.observe(this.target, {
childList: true,
attributes: false,
characterData: true,
subtree: true
});
}
}
}
export default RouteWatcher;

192
src/content/index.js Normal file
View File

@@ -0,0 +1,192 @@
import "src/stylus/content.styl";
import ChatScroller from "./ChatScroller";
import ChatWatcher from "./ChatWatcher";
import RouteWatcher from "./RouteWatcher";
import {
isLivestream, isYoutubeGaming,
isYoutubeEmbed, isYoutubeVanilla,
isPopOut
} from "src/helpers/Identification";
import PersistentSyncStorage from "src/helpers/PersistentSyncStorage";
let MAIN = null;
const theater_wrapper = document.createElement('theater_wrapper');
document.body.appendChild(theater_wrapper);
var alreadyTheater = false;
// ---
class Main {
constructor() {
this.chatWatcher = null;
this.chatScroller = null;
this.routeWatcher = null;
this.onRouteChange = this.onRouteChange.bind(this);
this.load();
}
load() {
this.routeWatcher = new RouteWatcher();
this.routeWatcher.on("change", this.onRouteChange);
this.onRouteChange();
}
onRouteChange() {
if(isLivestream() && ((isYoutubeGaming()) || (isYoutubeVanilla()) || (isYoutubeEmbed()) || isPopOut())) {
this.init();
}
if(isLivestream()) {
if (PersistentSyncStorage.data.options.theaterModeFix) {
if(document.getElementById('player-container') != null && document.getElementById('player-theater-container') != null){
theaterMode();
}
}
}
}// end onRouteChange
setDefaults() {
// Set Hide Welcome Banner
if (PersistentSyncStorage.data.options.alternateLineColor) {
document.querySelector('#items.style-scope.yt-live-chat-item-list-renderer').classList.add('alternateLineColor');
}
///////////////////////////////////////////////////////////////////
// Welcome Banner
var welcomBanner = document.querySelector("yt-live-chat-viewer-engagement-message-renderer");
// Set Hide Welcome Banner
if (PersistentSyncStorage.data.options.hideWelcomBanner) {
welcomBanner.classList.add("hideElement");
}
///////////////////////////////////////////////////////////////////
//Live Chat Default Option
if (PersistentSyncStorage.data.options.setLiveChat) {
document.getElementsByClassName("yt-simple-endpoint style-scope yt-dropdown-menu").item(1).click();
} else {
// do nothing, let user pick option if not set as default in options menu
}
///////////////////////////////////////////////////////////////////
//Live Chat Default Option
if (PersistentSyncStorage.data.options.setGreenScreen) {
var chat = document.querySelector('yt-live-chat-item-list-renderer');
chat.classList.add('green_sceen');
} else {
// do nothing, let user pick option if not set as default in options menu
}
}
init() {
this.chatWatcher = new ChatWatcher();
this.chatWatcher.init();
this.chatScroller = new ChatScroller();
this.chatScroller.init();
this.setDefaults();
console.log("INIT");
}// end init
}// end main
// --- Every Frame Loaded
PersistentSyncStorage.on("ready", () => {
MAIN = new Main();
});
function checkMode(){
if(alreadyTheater){
console.log('enterTheater');
alreadyTheater = false;
enterTheaterMode();
}else{
// is reverse because at the time of check dom elements havent moved yet
if(document.getElementById('player-theater-container').contains(document.getElementById('player-container'))){
console.log('exitTheater');
exitTheaterMode();
}else{
console.log('enterTheater');
enterTheaterMode();
}
}
}
function enterTheaterMode() {
const movie_player = document.getElementById('movie_player');
const chat_frame = document.getElementById('chatframe');
const info_frame = document.getElementById('info-contents');
const masthead_container = document.getElementById('masthead-container');
masthead_container.hidden = true;
theater_wrapper.classList.add('theater_wrapper_fix');
movie_player.classList.add('movie_player_fix');
chat_frame.classList.add('chat_frame_fix');
info_frame.classList.add('info_contents_fix');
theater_wrapper.append(info_frame);
theater_wrapper.append(movie_player);
theater_wrapper.append(chat_frame);
document.body.classList.add('body_Fix');
}// end enterTheaterMode
function exitTheaterMode(){
const movie_player = document.getElementById('movie_player');
const chat_frame = document.getElementById('chatframe');
const info_frame = document.getElementById('info-contents');
const movie_player_container = document.getElementById('player-container');
const player_container_parent = document.getElementById('player-container-inner');
const chat_frame_parent = document.getElementById('chat');
const info_frame_before = document.getElementById('meta');
const masthead_container = document.getElementById('masthead-container');
masthead_container.hidden = false;
theater_wrapper.classList.remove('theater_wrapper_fix');
movie_player.classList.remove('movie_player_fix');
chat_frame.classList.remove('chat_frame_fix');
info_frame.classList.remove('info_contents_fix');
movie_player_container.prepend(movie_player);
player_container_parent.prepend(movie_player_container);
chat_frame_parent.prepend(chat_frame);
info_frame_before.before(info_frame);
document.body.classList.remove('body_Fix');
}
function theaterMode(){
var theaterButton = document.querySelector('button.ytp-size-button.ytp-button');
if(theaterButton){
if(document.getElementById('player-theater-container').contains(document.getElementById('player-container'))){
// for when page loads first time - check is reversed after this
alreadyTheater = true;
checkMode();
}
// add button
theaterButton.addEventListener('click', checkMode, false);
}
}

View File

@@ -0,0 +1,48 @@
export const isLivestream = () => {
const timeDisplay = document.querySelector('.ytp-time-display');
const chatApp = document.querySelector('yt-live-chat-app');
const chatHeader = document.querySelector('.yt-live-chat-renderer-0');
const timeDisplayCheck = timeDisplay && timeDisplay.classList.contains('ytp-live');
const chatCheck = (document.body.contains(chatApp) || document.body.contains(chatHeader));
return (timeDisplayCheck || chatCheck);
};
// isYoutubeGaming checks for the presence of ytg-app, the top level element for YT Gaming
export const isYoutubeGaming = () => {
return !!document.querySelector('ytg-app');
};
// isYoutubeEmbed checks that this is an iframe, and it is being used on youtube.com
export const isYoutubeVanilla = () => {
// window.frameElement is only available from youtube.com sites from within iframe per CORS
return !!window.frameElement;
};
// isYoutubeEmbed checks that this is an iframe, and it is **not** loaded from youtube.com (main site uses embed too)
export const isYoutubeEmbed = () => {
// If the frameElement is available, then CORS means that we must be on youtube.com.
if (window.frameElement) {
return false;
}
// If the window location isn't the parent location, then we are in an iframe.
return (window.location != window.parent.location);
};
// isPopOut fix for popout page
export const isPopOut = () => {
// If the frameElement is available, then CORS means that we must be on youtube.com.
if (window.frameElement) {
return false;
}
// Checks href for page
if(window.location.href.includes('is_popout=1')){
return !!window.location.href.includes('popout=1');
}
return false;
};

View File

@@ -0,0 +1,13 @@
/* Open new tab if tab is not already open, otherwise focus that tab */
export default url => {
const matchUrl = url.replace(/^(https|http)/i, '*');
chrome.tabs.query({ url: matchUrl }, tabs => { // url must be valid match pattern - https://developer.chrome.com/extensions/match_patterns
if(tabs && tabs.length) {
// tab.id is not present in some rare cases, so if error around here, that could be the cause.
chrome.tabs.update(tabs[0].id, { active: true });
} else {
chrome.tabs.create({ url });
}
});
};

View File

@@ -0,0 +1,51 @@
import { SyncStorage } from '../utils/chrome';
import { EventEmitter } from 'events';
class PersistentSyncStorage extends EventEmitter {
constructor() {
super();
this._data = null;
this.state = 'initiating';
this._init();
}
async _init() {
const fetchedData = await SyncStorage.get();
this._initListener();
this._data = fetchedData;
this.state = 'ready';
this.emit(this.state);
}
_initListener() {
SyncStorage.listen((changes) => {
Object.keys(changes).forEach((changeKey) => {
if(changes[changeKey].hasOwnProperty('newValue')) {
this._data[changeKey] = changes[changeKey].newValue;
} else {
console.error('No newValue in sync storge change');
}
});
this.emit('change', this.data, changes);
});
}
set(items) {
return SyncStorage.set(items);
}
get data() {
return this._data;
}
has(item) {
return this.data.hasOwnProperty(item);
}
}
export default new PersistentSyncStorage();

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Chat Overlay</title>
<link rel="stylesheet" href="./youtube.css">
</head>
<body>
<div id="message_container"></div>
<script src="../chat_overlay.js"></script>
</body>
</html>

View File

@@ -1,31 +1,17 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<style>
body {
font-family: sans-serif;
padding: 10px;
width: 400px;
}
.hideDiv {
display: none;
}
</style>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Options</title> <title>WompChat Options</title>
</head> </head>
<body> <body>
<span style="display:inline-block;vertical-align: middle; margin-right: 2%;"> <div class="container">
<img src="../assets/icons/icon128.png" alt="🔴" style="height: 2.5em;"> <img src="../assets/icons/logo128.png" alt="🔴">
</span> <h1>WompChat Options</h1>
<span style="display:inline-block;vertical-align: middle;">
<h1>Live Chat Options</h1>
</span>
<span class="">
<button class="info_button" id="infoButton" >Info</button> <button class="info_button" id="infoButton" >Info</button>
</span> </div>
<br> <br>
<br> <br>
@@ -56,7 +42,7 @@
</div> </div>
</div> </div>
<div class="hr"></div> <hr>
<div class="options-table"> <div class="options-table">
<div class="option-row"> <div class="option-row">
@@ -67,8 +53,7 @@
</div> </div>
<hr>
<div class="hr"></div>
<div class="section"> <div class="section">
<h2 class="options-heading">Chat Options</h2> <h2 class="options-heading">Chat Options</h2>
@@ -130,13 +115,18 @@
</div> </div>
</div> </div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="setGreenScreen">Green Screen Chat</label></div>
<div class="option-cell"><input disabled type="checkbox" id="setGreenScreen" class="option-input"></div>
</div>
</div> </div>
<div class="hr"></div> </div>
<h2 class="options-heading">Font Size</h2>
<span>
</span> <hr>
<h2 class="options-heading">Font Size</h2>
<span> <span>
<div class="options-table"> <div class="options-table">
@@ -153,10 +143,12 @@
</div> </div>
</div> </div>
</span> </span>
<hr>
<!-- <button class="chat_overlay" id="chat_overlay" >Chat Overlay</button> -->
<div id="save-status">&nbsp;</div> <div id="save-status">&nbsp;</div>
<div class="omega"></div>
</div> </div>
<script src="../options.js"></script> <script src="../options.js"></script>

13
src/html/welcome.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Welcome Page</title>
<meta http-equiv="refresh" content="0; URL='https://wompmacho.com/wompchat'" />
</head>
<body>
<script src="../welcomePage.js"></script>
</body>
</html>

59488
src/html/youtube.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,11 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "🔴 LIVE CHAT", "name": "WompChat",
"version": "1.0.1", "version": "1.0.3",
"description": "Enhances the YouTube Live Streaming experience with Emotes, Custom Styling and quality of life improvements.", "description": "Enhances the YouTube Live Streaming experience with Emotes, Custom Styling and quality of life improvements.",
"icons": { "icons": {
"48": "assets/icons/icon48.png", "128": "assets/icons/logo128.png"
"128": "assets/icons/icon128.png",
"512": "assets/icons/icon512.png"
}, },
"permissions": [ "permissions": [

192
src/options.js Normal file
View File

@@ -0,0 +1,192 @@
import './stylus/options.styl';
import { debounce } from 'lodash';
import PersistentSyncStorage from './helpers/PersistentSyncStorage';
// hides element after short timeout
const hideDebounce = debounce(ele => {
ele.classList.remove('show');
}, 1000);
// little popup/fade save status message
const setSavingStatus = (status) => {
const SaveStatusEle = document.getElementById('save-status');
switch(status) {
case 'saving':
SaveStatusEle.innerHTML = 'Saving ...';
break;
case 'saved':
SaveStatusEle.innerHTML = 'Saved';
hideDebounce(SaveStatusEle);
break;
default:
SaveStatusEle.innerHTML = '&nbsp;';
}
SaveStatusEle.classList.add('show');
};
///////////////////////////////////////////////////////////////////////////////
var textSizeSlider = document.getElementById("textSizeSlider");
var allowTextSlider = document.getElementById("allowTextSlider");
var sliderValue;
textSizeSlider.oninput = function(){
sliderValue = textSizeSlider.value;
};
allowTextSlider.oninput = function(){
if(allowTextSlider.checked == true){
textSizeSlider.disabled = false;
}else {
textSizeSlider.disabled = true;
}
};
const optionOnChange = (input) => {
var inputValueKey;
if(input.id === 'textSizeSlider'){
inputValueKey = input.value;
if(PersistentSyncStorage.data.options.hasOwnProperty(input.id)) {
inputValueKey = PersistentSyncStorage.data.options[input.id];
textSizeSlider.value = inputValueKey;
}
}else{
inputValueKey = 'checked';
if(PersistentSyncStorage.data.options.hasOwnProperty(input.id)) {
input[inputValueKey] = PersistentSyncStorage.data.options[input.id];
}
}
const eventType = 'change';
const onChange = (() => {
const saveOption = () => {
setSavingStatus('saving');
// [input.id]: inputValueKey | number vs true or false statement| [input.id]: input[inputValueKey]
if(input.id === 'textSizeSlider'){
inputValueKey = sliderValue;
const updatedOptions = Object.assign({}, PersistentSyncStorage.data.options, {
[input.id]: inputValueKey
});
PersistentSyncStorage.set({ options: updatedOptions })
.then(() => {
setSavingStatus('saved');
});
}else{
const updatedOptions = Object.assign({}, PersistentSyncStorage.data.options, {
[input.id]: input[inputValueKey]
});
PersistentSyncStorage.set({ options: updatedOptions })
.then(() => {
setSavingStatus('saved');
});
}
};
return saveOption;
})();
return onChange;
};
// Executed code
const OptionInputs = document.querySelectorAll('.option-input');
PersistentSyncStorage.on('ready', () => {
OptionInputs.forEach((input) => {
const inputOnChange = optionOnChange(input);
input.addEventListener('change', inputOnChange);
switch (input.id) {
case 'enableBTTVEmotes':
input.removeAttribute('disabled');
break;
case 'enableFrankerEmotes':
input.removeAttribute('disabled');
break;
case 'enableTwitchEmotes':
input.removeAttribute('disabled');
break;
case 'kappaFix':
input.removeAttribute('disabled');
break;
case 'theaterModeFix':
input.removeAttribute('disabled');
break;
case 'setAuthorColor':
input.removeAttribute('disabled');
break;
case 'showTimeStamp':
input.removeAttribute('disabled');
break;
case 'alternateLineColor' :
input.removeAttribute('disabled');
break;
case 'hideAuthorIcons' :
input.removeAttribute('disabled');
break;
case 'hideWelcomBanner' :
input.removeAttribute('disabled');
break;
case 'setTwitchColors' :
input.removeAttribute('disabled');
break;
case 'setLiveChat' :
input.removeAttribute('disabled');
break;
case 'allowTextSlider':
input.removeAttribute('disabled');
if(PersistentSyncStorage.data.options.allowTextSlider == true){
textSizeSlider.disabled = false;
}else if(PersistentSyncStorage.data.options.allowTextSlider == false){
textSizeSlider.disabled = true;
}
break;
case 'setGreenScreen':
input.removeAttribute('disabled');
break;
default:
// shouldn't get hete if handled
break;
}
});
});
var infoButton = document.getElementById('infoButton');
infoButton.addEventListener('click', function(){
chrome.tabs.create({ url: 'https://wompmacho.com/wompchat' });
});
// var chat_overlay = document.getElementById('chat_overlay');
// chat_overlay.addEventListener('click', function(){
// chrome.tabs.create({ url: 'html/chat_overlay.html' });
// });

View File

@@ -0,0 +1,72 @@
colors = {
'A': #f44336,
'B': #e91e63,
'C': #9c27b0,
'D': #673ab7,
'E': #536dfe,
'F': #2196f3,
'G': #03a9f4,
'H': #00bcd4,
'I': #009688,
'J': #4caf50,
'K': #8bc34a,
'L': #cddc39,
'M': #ffeb3b,
'N': #ffc107,
'O': #ff9800,
'P': #ff5722,
'Q': #f44336,
'R': #e91e63,
'S': #9c27b0,
'T': #673ab7,
'U': #536dfe,
'V': #2196f3,
'W': #03a9f4,
'X': #00bcd4,
'Y': #009688,
'Z': #4caf50,
'a': #8bc34a,
'b': #cddc39,
'c': #ffeb3b,
'd': #ffc107,
'e': #ff9800,
'f': #ff5722,
'g': #f44336,
'h': #e91e63,
'i': #9c27b0,
'j': #673ab7,
'k': #536dfe,
'l': #2196f3,
'm': #03a9f4,
'n': #00bcd4,
'o': #009688,
'p': #4caf50,
'q': #8bc34a,
'r': #cddc39,
's': #ffeb3b,
't': #ffc107,
'u': #ff9800,
'v': #ff5722,
'w': #f44336,
'x': #e91e63,
'y': #9c27b0,
'z': #673ab7,
'0': #536dfe,
'1': #2196f3,
'2': #03a9f4,
'3': #00bcd4,
'4': #009688,
'5': #4caf50,
'6': #8bc34a,
'7': #cddc39,
'8': #ffeb3b,
'9': #ffc107,
'-': #ff9800,
'_': #ff5722,
}
for id, col in colors
.chat-color-{id} #content #author-name
color: col !important

View File

@@ -0,0 +1,40 @@
html
overflow: scroll;
overflow-x: hidden;
background-color:#00ff00 !important
::-webkit-scrollbar
width: 0px;
background: transparent;
::-webkit-scrollbar-thumb
background: #FF0000;
yt-live-chat-text-message-renderer
font-family: Impact, Charcoal, sans-serif !important
background-color: transparent !important
#timestamp
font-size: 1.5em !important
vertical-align: middle !important
color: white !important
font-weight: 700 !important
img
vertical-align: middle !important
height: 2em !important;
width: auto !important;
#author-name
font-size: 1.5em !important
vertical-align: middle !important
color: white !important
font-weight: 700 !important
#message
font-size: 2em !important
color: white !important
#Emote
vertical-align: middle
.message_container
overflow-y: scroll !important;
scroll-behavior: smooth

222
src/stylus/content.styl Normal file
View File

@@ -0,0 +1,222 @@
@import '_chatColors.styl'
.item-offset
//height: 100%
#items
// Chat user avatar
img.yt-img-shadow
//height: inherit;
//width: inherit;
//align-content: center;
//display: initial;
//border-radius: inherit;
// Owner chat messages
.yt-live-chat-item-list-renderer[author-type="owner"]
//background: rgba(#fff, 0.1);
#content #author-name
//text-shadow: #ffd600 0px 0px 10px
// Chat user menu button (3 dot menu)
#menu #menu-button.yt-live-chat-text-message-renderer
//padding: 5px !important
//width: 28px !important
//height: 28px !important
// Chat user name
#author-name.yt-live-chat-text-message-renderer
&:after
//content: ' :'
//color: #fff
.AuthorFix
#author-photo.yt-live-chat-text-message-renderer
img.yt-img-shadow
display: inline-block
vertical-align: middle
border-radius: 50%;
height: 1.5em;
width: 1.5em;
vertical-align: sub;
#content
display: inline-block
vertical-align: middle
.showTimeStamp
#timestamp.yt-live-chat-text-message-renderer
display: inline-block
vertical-align: middle
.Emote
height: 100%;
vertical-align: bottom;
img
height: 1.75em
align-self: center
display:inline-block
vertical-align: middle !important
emote_div:hover
background-size: 100% //100%
background-color: var(--divider-color)
.hideElement
display: none !important
.alternateLineColor
yt-live-chat-text-message-renderer:nth-child(odd)
background-color: rgba(255, 255, 255, 0.1) !important;
.setTwitchColors
text-shadow: 0 0 1px #000, 0 0 2px #000 !important
background: #18181b !important
font-family: 'Roboto' !important
font-size: 1.3rem !important
line-height: 1.5em !important
color: #FAFAFA !important
#timestamp.yt-live-chat-text-message-renderer
display: none
yt-live-chat-author-chip[is-highlighted] #author-name.owner.yt-live-chat-author-chip, #author-name.owner.yt-live-chat-author-chip
background-color: transparent
color: green
yt-live-chat-author-chip[is-highlighted] #author-name.yt-live-chat-author-chip
background-color: transparent
.emoteDivider
width: 20vw
border: 1px solid red;
border-radius: 5px;
margin-top: 1em;
margin-bottom: 1em;
margin-left: auto
margin-right: auto
.emotePopUpText
margin-bottom: 2%;
.popup
background-color: var(--yt-app-background)
color: var(--yt-live-chat-primary-text-color)
position: absolute;
top: 15%
left: 0
right: 0
margin-left: auto
margin-right: auto
height 70%
width: 100%
z-index: 999
text-align: center
border-radius: 5px
border: #303030 1px solid
font-size: 1em
overflow: hidden
overflow-y: scroll
padding: 1%;
padding-top: 2%;
.emoteButton
background-color:rgba(255, 255, 255, .1);
background-image: url("https://cdn.frankerfacez.com/emoticon/447885/4")
background-repeat: no-repeat
background-position: center
background-size: 80% 80%;
width: var(--yt-live-chat-32px-icon-button_-_width)
height: var(--yt-live-chat-32px-icon-button_-_height)
padding: var(--yt-live-chat-32px-icon-button_-_padding)
border-radius: 10px;
border: none
cursor: pointer
.emoteButton:hover
background-color: #4e4e4e;
.emoteButton:focus
outline:0
.body_Fix
height: 100% !important
margin: 0 !important
overflow: hidden !important
.theater_wrapper_fix
padding: 2px;
background-color: #1e1e1e;
position: fixed
z-index: 900 !important
height: 100vh !important
width: 100vw !important
.movie_player_fix
height: 100vh;
position: absolute;
width: calc(100vw - 25vw);
video
left: 0 !important
top: 0 !important
height: 100vh !important
width: calc(100vw - 25vw) !important
#html5-video-player
top: 0 !important
width: calc(100vw - 25vw) !important
.ytp-title
color: rgba(255, 255, 255, .8) !important
.ytp-title-channel
all: unset
.ytp-gradient-top
max-width: calc(100vw - 25vw) !important
.ytp-chrome-bottom
width: calc(100vw - 25vw) !important
left: 0 !important
.html5-endscreen
width: calc(100vw - 25vw) !important
.ytp-chapter-hover-container
width: calc(100vw - 25vw) !important
.ytp-gradient-bottom
width: calc(100vw - 25vw) !important
.ytp-iv-video-content
width: calc(100vw - 25vw) !important
left: 0 !important
.ytp-player-content.ytp-iv-player-content
width: calc(100vw - 25vw) !important
left: -12px !important
bottom: 10vh !important;
.ytp-upnext.ytp-player-content.ytp-upnext-autoplay-paused.ytp-suggestion-set
width: calc(100vw - 25vw) !important
left: 0 !important
.ytp-bezel-text-hide
width: calc(100vw - 25vw) !important
left: 0 !important
.ytp-spinner
left: 40% !important
.ytp-cued-thumbnail-overlay
width: calc(100vw - 25vw) !important
.info_contents_fix
z-index: 901 !important
height: 80px
top: 0
position: absolute !important
width: calc(100vw - 25vw) !important
ytd-video-primary-info-renderer
padding: 1rem
border-bottom: none
.chat_frame_fix
height: 100vh !important
width: calc(calc(100vw - 75vw) - 1px) !important;
position: absolute !important
right: 0px !important
top: 0px !important
border: 1px solid #4e4e4e
.green_sceen
background-color: #00ff00 !important
font-weight: 700 !important
-webkit-text-stroke: 1px #000000
yt-live-chat-text-message-renderer:nth-child(odd)
background-color: #00ff00 !important

89
src/stylus/options.styl Normal file
View File

@@ -0,0 +1,89 @@
body
background-color: #1e1e1e
color: #ffffff
font-family: sans-serif;
padding: 10px;
width: 400px;
h1{
padding-left: 1em;
}
.hideDiv {
display: none;
}
.container{
display: flex;
vertical-align: middle;
height: 3.5em;
}
img{
max-height: 100%;
}
hr
border-color: #f00;
margin: auto;
margin-top: 1em;
margin-bottom: 1em;
.section
&:nth-child(1)
.options-heading
margin: 0
.options-heading
margin: 10px 0 0 0
.options-description
margin: 0 0 10px 0
opacity: 0.8
table
border: none
border-spacing: 0
width: 100%
tr td
padding: 0
&:nth-child(1)
width: 80%
&:nth-child(2)
width: 20%
text-align: right
#save-status
text-align: center
opacity: 0
transition: all 1s linear
&.show
opacity: 1
transition: none
.options-table
.option-row
display:flex
flex-direction: row
align-items: space-between
justify-content: space-between
.option-cell
padding-right: 5px
&:last-child
padding-right: 0px
.light-text
color: rgba(#000, 0.6)
input[type=checkbox][disabled]
outline: 1px solid red
cursor: not-allowed
opacity: .9
.info_button
border-radius:2em
margin-left: 135px;
transition: all 0.2s
.info_button:hover
color: red

View File

@@ -0,0 +1,10 @@
import Storage from './Storage';
class LocalStorage extends Storage {
constructor() {
super();
this.store = 'local';
}
}
export default LocalStorage;

View File

@@ -0,0 +1,90 @@
class Notifications {
create(notificationId = null, options) {
// notificationId is optional
if(typeof notificationId === 'object') {
options = notificationId;
notificationId = null;
}
return new Promise((res, rej) => {
// resolve args = notificationId:string
chrome.notifications.create(notificationId, options, res);
});
}
update(notificationId, options) {
return new Promise((res, rej) => {
// resolve args = wasUpdated:boolean
chrome.notifications.update(notificationId, options, res);
});
}
clear(notificationId) {
return new Promise((res, rej) => {
// resolve args = wasCleared:boolean
chrome.notifications.clear(notificationId, res);
});
}
getAll() {
return new Promise((res, rej) => {
// resolve args = notifications:object
chrome.notifications.getAll(res);
});
}
getPermissionLevel() {
return new Promise((res, rej) => {
// resolve args = level:PermissionLevel (https://developer.chrome.com/apps/notifications#type-PermissionLevel)
chrome.notifications.getPermissionLevel(res);
});
}
listen(event, notificationId = null, callback) {
// event = 'onClosed' | 'onClicked' | 'onButtonClicked' | 'onPermissionLevelChanged' | 'onShowSettings'
// notificationId is optional
if(typeof notificationId === 'function') {
callback = notificationId;
notificationId = null;
}
if(event === 'onPermissionLevelChanged' || event === 'onShowSettings') {
return this._nonNotificationIdListen(event, callback);
}
/**
* https://developer.chrome.com/apps/notifications#events
*
* Resolve args (by event):
* onClosed = notificationId:string, byUser:boolean
* onClicked = notificationId:string
* onButtonClicked = notificationId:string, buttonIndex:integer
*
* onPermissionLevelChanged = level:PermissionLevel (https://developer.chrome.com/apps/notifications#type-PermissionLevel)
* onShowSettings = (none)
*/
// This callback relates only to those events that have notificationId arg
const ListenerCallback = (() => {
if(notificationId !== null) {
return (passedNotificationId, ...args) => {
if(notificationId === passedNotificationId) {
callback(passedNotificationId, ...args);
}
};
} else {
return callback;
}
})();
chrome.notifications[event].addListener(ListenerCallback);
}
_nonNotificationIdListen(event, callback) {
chrome.notifications[event].addListener(callback);
}
}
export default Notifications;

View File

@@ -0,0 +1,67 @@
class Storage {
get(keys = null) {
return new Promise((res, rej) => {
const returnSingle = typeof keys === 'string' || typeof keys === 'number';
// resolve args = items:object
chrome.storage[this.store].get(keys, (items) => {
if(returnSingle) {
res(items[keys]);
} else {
res(items);
}
});
});
}
getBytesInUse(keys = null) {
return new Promise((res, rej) => {
// resolve args = bytesInUse:integer
chrome.storage[this.store].getBytesInUse(keys, res);
});
}
set(items) {
return new Promise((res, rej) => {
// resolve args = (none)
chrome.storage[this.store].set(items, res);
});
}
remove(keys) {
// resolve args = (none)
return new Promise((res, rej) => {
chrome.storage[this.store].remove(keys, res);
});
}
clear() {
// resolve args = (none)
return new Promise((res, rej) => {
chrome.storage[this.store].clear(res);
});
}
listen(item, onChange) {
if(typeof item === 'function') {
onChange = item;
item = null;
}
chrome.storage.onChanged.addListener((changes, areaName) => {
if(areaName === this.store) {
if(item !== null) {
if(changes.hasOwnProperty(item)) {
const oldValue = changes[item].oldValue || null;
const newValue = changes[item].newValue || null;
onChange(oldValue, newValue);
}
} else {
onChange(changes);
}
}
});
}
}
export default Storage;

View File

@@ -0,0 +1,10 @@
import Storage from './Storage';
class SyncStorage extends Storage {
constructor() {
super();
this.store = 'sync';
}
}
export default SyncStorage;

View File

@@ -0,0 +1,7 @@
import _LocalStorage from './LocalStorage';
import _SyncStorage from './SyncStorage';
import _Notifications from './Notifications';
export const LocalStorage = new _LocalStorage;
export const SyncStorage = new _SyncStorage;
export const Notifications = new _Notifications;

27
src/welcomePage.js Normal file
View File

@@ -0,0 +1,27 @@
import PersistentSyncStorage from './helpers/PersistentSyncStorage';
const setupComplete = () => {
};
// --- Main ---
const main = () => {
PersistentSyncStorage.on('ready', () => {
if(!!PersistentSyncStorage.data.setupComplete === true) {
console.log('Setup is already complete, Should not be here');
return true;
}
let setupOutput = {};
chrome.runtime.sendMessage({
name: 'setupComplete',
data: setupOutput
}, setupComplete);
console.log('Competed setup, sent message');
});
};
// --- Executed ---
main();

54
webpack.config.js Normal file
View File

@@ -0,0 +1,54 @@
// Packages
const webpack = require('webpack'),
path = require('path');
// Webpack Plugins
const CopyPlugin = require('copy-webpack-plugin');
// Paths
const srcPath = path.join(__dirname, 'src'),
distPath = path.join(__dirname, 'dev-build'),
node_modulesPath = path.join(__dirname, 'node_modules');
module.exports = {
mode: 'development',
resolve: {
alias: {
src: srcPath
}
},
context: srcPath,
entry: {
content: './content/',
background: './background/',
options: './options.js',
welcomePage: './welcomePage.js',
chat_overlay: './chat_overlay.js'
},
output: {
path: distPath,
filename: './[name].js'
},
module: {
rules: [
{ test: /\.styl$/, use: ['style-loader', 'css-loader', 'stylus-loader'], exclude: /node_modules/ }
]
},
plugins: [
new CopyPlugin([
'manifest.json',
'html/**/*',
'assets/**/*'
], {
ignore: [
'**/*.psd'
]
})
],
devtool: '#inline-cheap-source-map'
};

63
webpack.prod.js Normal file
View File

@@ -0,0 +1,63 @@
// Packages
const webpack = require('webpack'),
path = require('path');
// Webpack Plugins
const CopyPlugin = require('copy-webpack-plugin'),
UglifyJsPlugin = require('uglifyjs-webpack-plugin');
// Paths
const srcPath = path.join(__dirname, 'src'),
distPath = path.join(__dirname, 'build'),
node_modulesPath = path.join(__dirname, 'node_modules');
module.exports = {
mode: 'production',
resolve: {
alias: {
src: srcPath
}
},
context: srcPath,
entry: {
content: './content/',
background: './background/',
options: './options.js',
welcomePage: './welcomePage.js',
chat_overlay: './chat_overlay.js'
},
output: {
path: distPath,
filename: './[name].js'
},
module: {
rules: [
{ test: /\.styl$/, use: ['style-loader', 'css-loader', 'stylus-loader'], exclude: /node_modules/ }
]
},
plugins: [
new CopyPlugin([
'manifest.json',
'html/**/*',
'assets/**/*',
], {
ignore: [
'**/*.psd'
]
}),
new UglifyJsPlugin({
uglifyOptions: {
output: {
beautify: false,
comments: false
}
}
})
],
devtool: '#inline-cheap-source-map'
};

File diff suppressed because one or more lines are too long