17 Commits

Author SHA1 Message Date
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
62 changed files with 7343 additions and 26264 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
dev-build
node_modules
build

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

20531
options.js

File diff suppressed because one or more lines are too long

4714
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "LiveChat",
"version": "1.0.2",
"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": {
"axios": "^0.19.0",
"date-fns": "^1.29.0",
"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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
src/assets/icons/3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
src/assets/icons/blob.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
src/assets/icons/icon.psd Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
src/assets/icons/icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 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('Setup Complete');
});
};
export default {ensure};

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

@@ -0,0 +1,27 @@
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();

26
src/config.js Normal file
View File

@@ -0,0 +1,26 @@
const CONFIG = {
defaultOptions: {
// Emote Options
enableBTTVEmotes: true,
enableFrankerEmotes: true,
enableTwitchEmotes: true,
kappaFix: true,
// Chat Options
theaterModeFix: false,
setAuthorColor: false,
showTimeStamp: false,
alternateLineColor: false,
hideAuthorIcons: false,
hideWelcomBanner: false,
setTwitchColors: false,
textSizeSlider: 'inherit',
setLiveChat: true,
allowTextSlider: false,
}
};
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.scrollTop = 9999;
}
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;

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

@@ -0,0 +1,227 @@
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);
}
}
}
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);
}
}
}
// 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');
});
// 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;

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

@@ -0,0 +1,194 @@
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];
}
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;

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

@@ -0,0 +1,237 @@
import Emotes from './Emotes';
import PersistentSyncStorage from 'src/helpers/PersistentSyncStorage';
var colorNumberIndex = 0;
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");
}
///////////////////////////////////////////////////////////////////
// Set Alternate message Colors
if (PersistentSyncStorage.data.options.alternateLineColor) {
this.alternateLineColor();
}
}// 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);
}
}
}
}
///////////////////////////////////////////////////////////////////
// changes color every line
alternateLineColor(){
if(colorNumberIndex % 2 == 0){
this.node.classList.add("set-background-color-one");
}
if(colorNumberIndex % 2 !== 0){
this.node.classList.add("set-background-color-two");
}
colorNumberIndex++;
}
///////////////////////////////////////////////////////////////////
// removes color attr
removelternateLineColor(){
this.node.classList.remove("set-background-color-one");
this.node.classList.remove("set-background-color-two");
}
}// 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;

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

@@ -0,0 +1,188 @@
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();
// button class - ytp-size-button ytp-button
// right player controls - ytp-right-controls
// player div id - ytd-player
// chatframe id - chatframe
// movieframe id - movie_player_fix
// dono ticker id - ticker
// player-theater-container
}
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() {
///////////////////////////////////////////////////////////////////
// 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
}
///////////////////////////////////////////////////////////////////
}
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,20 @@
/* 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 });
}
});
// for(let i = 0, tab; tab = tabs[i]; i++) {
// if(tab.url && tab.url === url) {
// return;
// }
// }
};

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();

165
src/html/options.html Normal file
View File

@@ -0,0 +1,165 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: sans-serif;
padding: 10px;
width: 400px;
}
.hideDiv {
display: none;
}
</style>
<meta charset="UTF-8">
<title>Options</title>
</head>
<body>
<span style="display:inline-block;vertical-align: middle; margin-right: 2%;">
<img src="../assets/icons/icon128.png" alt="🔴" style="height: 2.5em;">
</span>
<span style="display:inline-block;vertical-align: middle;">
<h1>Live Chat Options</h1>
</span>
<span class="">
<button class="info_button" id="infoButton" >Info</button>
</span>
<br>
<br>
<div id="optionsMenu" class="tabcontent active">
<div class="section">
<h2 class="options-heading">Emote Options</h2>
<div class="options-description"></div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="enableBTTVEmotes">Enable BTTV - Top, Trending and Global Emotes (200+)</label></div>
<div class="option-cell"><input disabled type="checkbox" id="enableBTTVEmotes" class="option-input"></div>
</div>
</div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="enableFrankerEmotes">Enable FrankerFacez - Top 100 emotes</label></div>
<div class="option-cell"><input disabled type="checkbox" id="enableFrankerEmotes" class="option-input"></div>
</div>
</div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="enableTwitchEmotes">Enable Twitch - Global emotes</label></div>
<div class="option-cell"><input disabled type="checkbox" id="enableTwitchEmotes" class="option-input"></div>
</div>
</div>
<div class="hr"></div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="kappaFix">Kappa Fix</label></div>
<div class="option-cell"><input disabled type="checkbox" id="kappaFix" class="option-input"></div>
</div>
</div>
</div>
<div class="hr"></div>
<div class="section">
<h2 class="options-heading">Chat Options</h2>
<div class="options-description"></div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="setLiveChat">Make LIVE CHAT Default</label></div>
<div class="option-cell"><input disabled type="checkbox" id="setLiveChat" class="option-input"></div>
</div>
</div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="theaterModeFix">Improved Theater Mode</label></div>
<div class="option-cell"><input disabled type="checkbox" id="theaterModeFix" class="option-input"></div>
</div>
</div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="setTwitchColors">Twitch Styling</label></div>
<div class="option-cell"><input disabled type="checkbox" id="setTwitchColors" class="option-input"></div>
</div>
</div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="hideAuthorIcons">Hide Author Icons</label></div>
<div class="option-cell"><input disabled type="checkbox" id="hideAuthorIcons" class="option-input"></div>
</div>
</div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="showTimeStamp">Show TimeStamp</label></div>
<div class="option-cell"><input disabled type="checkbox" id="showTimeStamp" class="option-input"></div>
</div>
</div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="hideWelcomBanner">Hide Welcome Banner</label></div>
<div class="option-cell"><input disabled type="checkbox" id="hideWelcomBanner" class="option-input"></div>
</div>
</div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="alternateLineColor">Alternate Line Colors</label></div>
<div class="option-cell"><input disabled type="checkbox" id="alternateLineColor" class="option-input"></div>
</div>
</div>
<div class="options-table">
<div class="option-row">
<div class="option-cell"><label for="setAuthorColor">Colorful User Names</label></div>
<div class="option-cell"><input disabled type="checkbox" id="setAuthorColor" class="option-input"></div>
</div>
</div>
</div>
<div class="hr"></div>
<h2 class="options-heading">Font Size</h2>
<span>
</span>
<span>
<div class="options-table">
<div class="option-row">
<div class="slidecontainer">
<input disabled type="range" min="1" max="50" value="13" class="slider option-input" id="textSizeSlider">
</div>
<div class="option-cell">
<label for="allowTextSlider"></label>
</div>
<div class="option-cell">
<input disabled type="checkbox" id="allowTextSlider" class="option-input">
</div>
</div>
</div>
</span>
<div id="save-status">&nbsp;</div>
<div class="omega"></div>
</div>
<script src="../options.js"></script>
</body>
</html>

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

@@ -0,0 +1,96 @@
<!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><s>Theater Mode Fix for that quality Stream and Chat time</s></li>
<li>Moderation Options</li>
<li>User Profile Info</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="emotesMenu.gif">
<p>
Enhanced Theater Mode Is Now Available. Just click the check in the option panel and refresh your page.
</p>
<img style="height: 540px; width: 960px;" src="../assets/gif/theaterMode.gif" alt="theaterMode.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="optionsMenu.gif">
<p>
Can Also Click on UserNames To Autofill an @ Notification
</p>
<img src="../assets/gif/clickUserNamesToAtThem.gif" alt="clickUserNamesToAtThem.gif">
<h2>Use At Your Own Risk</h2>
<h1>Enjoy!</h1>
</div>
<script src="../welcomePage.js"></script>
</body>
</html>

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "🔴 LIVE CHAT",
"version": "1.0.1",
"version": "1.0.2",
"description": "Enhances the YouTube Live Streaming experience with Emotes, Custom Styling and quality of life improvements.",
"icons": {
"48": "assets/icons/icon48.png",

155
src/options.js Normal file
View File

@@ -0,0 +1,155 @@
import './stylus/options.styl';
import dateFormat from 'date-fns/format';
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 'allowTextSlider':
input.removeAttribute('disabled');
if(PersistentSyncStorage.data.options.allowTextSlider == true){
textSizeSlider.disabled = false;
}else{
textSizeSlider.disabled = true;
}
break;
case 'theaterModeFix':
// do nothing, stay disabled
input.removeAttribute('disabled');
break;
case 'textSizeSlider' :
// do nothing
break;
default:
input.removeAttribute('disabled');
break;
}
});
});
var infoButton = document.getElementById('infoButton');
infoButton.addEventListener('click', function(){
chrome.tabs.create({ url: './html/welcome.html' });
});

View File

@@ -0,0 +1,73 @@
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

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

@@ -0,0 +1,223 @@
@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
#timestamp.yt-live-chat-text-message-renderer
font-size: 1em
display: inline-block
vertical-align: middle
.showTimeStamp
#timestamp.yt-live-chat-text-message-renderer
display: inline-block
.Emote
img
height: 1.75em
align-self: center
vertical-align: sub;
display:inline-block
vertical-align: middle
emote_div:hover
background-size: 100% //100%
background-color: rgba(255, 255, 255, .6)
.hideElement
display: none !important
.set-background-color-one
background-color: #303030 !important
.set-background-color-two
background-color: transparent !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: 60vw
border: 2px solid #d3d3d3;
border-radius: 5px;
margin-top: 2%;
margin-bottom: 2%;
margin-left: auto
margin-right: auto
.emotePopUpText
margin-bottom: 2%;
.popup
background-color: #202020
position: absolute;
top: 15%
left: 0
right: 0
margin-left: auto
margin-right: auto
height 70%
width: 75%
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:rgba(255, 255, 255, .8)
.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

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

@@ -0,0 +1,76 @@
body
background-color: #1e1e1e
color: #ffffff
.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
.hr
background-color: #d1d1d1
height: 1px
margin: 10px 0
#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
display:inline-block
padding:0.3em 1.2em
margin:0 0.3em 0.3em 0
border-radius:2em
box-sizing: border-box
text-decoration:none
font-weight:300
color:#1e1e1e
text-align:center
margin-left: 150px
transition: all 0.2s
.info_button:hover
color: red

146
src/stylus/setupPage.styl Normal file
View File

@@ -0,0 +1,146 @@
@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,700|Material+Icons")
body
font-family: 'Roboto', Arial, sans-serif
font-size: 16px
background: #222
color: #fff
margin: 25px
font-weights = {
light: 300,
regular: 400,
bold: 700
}
for fw, w in font-weights
.fw-{fw}
font-weight: w
.success-overlay
visibility: hidden
opacity: 0
position: fixed
top: 0
left: 0
z-index: 10
background: rgba(#191919, 0.95)
display: flex
flex-direction: column
align-items: center
justify-content: center
height: 100%
width: 100%
transition: visibility 0s, opacity 200ms ease
&.show
visibility: visible
opacity: 1
.material-icons
font-size: 20em
opacity: 0
&.show
opacity: 1
animation: jackInTheBox 700ms 1
.success-message
font-size: 3em
.close-message
font-size: 0.7em
opacity: 0.7
.container
display: flex
flex-direction: column
align-items: center
h1
margin: 0
font-size: 3em
letter-spacing: 3px
.heading-note
font-size: 18px
color: #bbb
.option
text-align: center
.option-note
font-size: 12px
font-style: italic
color: #999
.complete-setup-button
background: #333
display: flex
justify-content: center
align-items: center
padding: 10px
cursor: pointer
text-transform: uppercase
border-radius: 3px
box-shadow: 0 2px 5px 0 rgba(#000, 0.7)
transition: background 50ms ease
&:hover
background: #404040
.material-icons
margin-right: 7px
.links_bottom
display: inline-block;
margin: 1%
a
background-color: #252526;
color: white;
padding-top: 1em;
padding-bottom: 1em;
text-decoration: none;
text-transform: uppercase;
width: 100%;
table-layout: fixed;
border-collapse: collapse;
text-align: center
border: 1px solid #4e4e4e
a:hover
background-color: #d3d3d3;
color: black
a:active
box-shadow: none;
top: 5px;
.links_div
width: 20vw
p
margin: 2%
max-width: 30vw
h2
margin: 2%
hr {
margin: 2%
width: 60vw
border: 3px solid #d3d3d3;
border-radius: 5px;
}
// jackInTheBox from animate.css https://github.com/daneden/animate.css
@keyframes jackInTheBox
from
opacity: 0
transform: scale(0.1) rotate(60deg)
transform-origin: center center
50%
transform: rotate(-20deg)
70%
opacity: 1
transform: rotate(6deg)
to
transform: scale(1)

View File

@@ -0,0 +1,35 @@
/**
* 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.
*/
import Storage from './Storage';
class LocalStorage extends Storage {
constructor() {
super();
this.store = 'local';
}
}
export default LocalStorage;

View File

@@ -0,0 +1,115 @@
/**
* 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.
*/
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,92 @@
/**
* 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.
*/
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,35 @@
/**
* 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.
*/
import Storage from './Storage';
class SyncStorage extends Storage {
constructor() {
super();
this.store = 'sync';
}
}
export default SyncStorage;

37
src/utils/chrome/index.js Normal file
View File

@@ -0,0 +1,37 @@
/**
* 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.
*/
import _LocalStorage from './LocalStorage';
import _SyncStorage from './SyncStorage';
import _Notifications from './Notifications';
// export default {
// LocalStorage: new _LocalStorage,
// SyncStorage: new _SyncStorage,
// Notifications: new _Notifications
// }
export const LocalStorage = new _LocalStorage;
export const SyncStorage = new _SyncStorage;
export const Notifications = new _Notifications;

71
src/welcomePage.js Normal file
View File

@@ -0,0 +1,71 @@
import './stylus/setupPage.styl';
import PersistentSyncStorage from './helpers/PersistentSyncStorage';
// THIS IS A JS PAGE FOR SETUP.HTML
// --- Definitions ---
const completeButton = document.querySelector('.complete-setup-button');
const successOverlay = document.querySelector('.success-overlay');
const successIcon = successOverlay.querySelector('.material-icons');
const successCloseMessageCountdown = successOverlay.querySelector('.countdown');
const setupComplete = () => {
// successOverlay.classList.add('show');
// setTimeout(() => {
// successIcon.classList.add('show');
// }, 100);
//////////////////////////////////// Neat if I ever want to close a window
// let closeCountdown = 5; // seconds
// const closeTimeout = () => {
// successCloseMessageCountdown.innerHTML = closeCountdown;
// setTimeout(() => {
// if(closeCountdown > 1) {
// closeCountdown -= 1;
// closeTimeout();
// } else {
// chrome.tabs.getCurrent((tab) => {
// chrome.tabs.remove(tab.id);
// });
// }
// }, 1000);
// }
// successCloseMessageCountdown.innerHTML = closeCountdown;
// closeTimeout();
};
// --- 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();

53
webpack.config.js Normal file
View File

@@ -0,0 +1,53 @@
// 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'
},
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'
};

62
webpack.prod.js Normal file
View File

@@ -0,0 +1,62 @@
// 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'
},
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