Extension converted to TypeScript
This commit is contained in:
@@ -1,691 +0,0 @@
|
||||
(function runTwitchNotes() {
|
||||
function save(filename, data) {
|
||||
const blob = new Blob([data], {type: 'text/json'});
|
||||
const elem = window.document.createElement('a');
|
||||
elem.href = window.URL.createObjectURL(blob);
|
||||
elem.download = filename;
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
document.body.removeChild(elem);
|
||||
}
|
||||
|
||||
const Notes = {
|
||||
inputData: {},
|
||||
getSavedUserList: () => {
|
||||
return JSON.parse(localStorage.getItem(LS_UserList) || "[]");
|
||||
},
|
||||
addUserToSavedList: (username) => {
|
||||
let userList = Notes.getSavedUserList();
|
||||
|
||||
if (!userList.includes(username)) {
|
||||
userList.push(username);
|
||||
localStorage.setItem(LS_UserList, JSON.stringify(userList));
|
||||
}
|
||||
},
|
||||
removeUserFromSavedList: (username) => {
|
||||
let userList = Notes.getSavedUserList();
|
||||
userList = userList.filter(u => u !== username);
|
||||
if (userList.length) {
|
||||
localStorage.setItem(LS_UserList, JSON.stringify(userList));
|
||||
} else {
|
||||
localStorage.removeItem(LS_UserList);
|
||||
}
|
||||
},
|
||||
saveNote: (username, note) => {
|
||||
Notes.addUserToSavedList(username);
|
||||
localStorage.setItem(LS_Prefix + username, note);
|
||||
},
|
||||
deleteNote: (username) => {
|
||||
Notes.removeUserFromSavedList(username);
|
||||
localStorage.removeItem(LS_Prefix + username);
|
||||
},
|
||||
getNote: (username) => {
|
||||
return localStorage.getItem(LS_Prefix + username) || "";
|
||||
},
|
||||
exportNotes: () => {
|
||||
save('twitch-notes-' + (Date.now()) + '.json', JSON.stringify({
|
||||
users: Notes.getSavedUserList(),
|
||||
notes: Notes.getSavedUserList().map(user => {
|
||||
return {user: user, note: Notes.getNote(user)};
|
||||
}),
|
||||
settings: {},
|
||||
}));
|
||||
},
|
||||
showAllNotes: () => {
|
||||
let activeNote = null;
|
||||
const blurredBackground = createElement('div', null, 'twitch-notes-blurred-background');
|
||||
const container = createElement('div', null, 'twitch-notes-center-floating-container', {
|
||||
padding: '0',
|
||||
paddingTop: '1rem',
|
||||
width: '100%',
|
||||
height: 'calc(320px + 5rem)'
|
||||
});
|
||||
const closeButton = createElement('button', xButton, 'twitch-notes-settings-close-button', {
|
||||
position: 'absolute',
|
||||
top: '1rem',
|
||||
right: '1rem',
|
||||
});
|
||||
|
||||
const title = createElement('div', 'Notes:', 'twitch-note-title', {
|
||||
borderBottom: '1px solid #FFFFFF19',
|
||||
paddingBottom: '1rem',
|
||||
});
|
||||
container.appendChild(title);
|
||||
|
||||
const content = createElement('div', null, null, {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'no-wrap',
|
||||
width: '100%',
|
||||
height: '320px'
|
||||
});
|
||||
container.appendChild(content);
|
||||
closeButton.addEventListener('click', () => {
|
||||
blurredBackground.remove();
|
||||
});
|
||||
container.appendChild(closeButton);
|
||||
|
||||
const userList = createElement('div', null, null, {
|
||||
borderRight: '1px solid #FFFFFF19',
|
||||
height: '320px',
|
||||
minWidth: '240px',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
});
|
||||
|
||||
const noteContainer = createElement('div', null, null, {
|
||||
height: '320px',
|
||||
padding: '1rem',
|
||||
width: '100%',
|
||||
opacity: '0',
|
||||
});
|
||||
|
||||
const note = createElement('div', null, null, {
|
||||
marginBottom: '3rem',
|
||||
border: '1px solid #FFFFFF19',
|
||||
height: 'calc(320px - 7rem)',
|
||||
width: '100%',
|
||||
padding: '6px',
|
||||
outline: 'none',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
}, {
|
||||
contentEditable: 'true',
|
||||
});
|
||||
const saveNote = createElement('button', 'Save Note', 'twitch-notes-settings-button', {
|
||||
position: 'absolute',
|
||||
bottom: '1rem',
|
||||
left: '1rem'
|
||||
});
|
||||
|
||||
saveNote.addEventListener('click', () => {
|
||||
if(!activeNote) {
|
||||
return;
|
||||
}
|
||||
Notes.saveNote(activeNote, note.innerHTML);
|
||||
})
|
||||
|
||||
for(let user of Notes.getSavedUserList()) {
|
||||
const userLine = createElement('div', user, 'twitch-notes-user-list-user');
|
||||
userLine.addEventListener('click', () => {
|
||||
const list = document.getElementsByClassName('twitch-notes-user-list-user');
|
||||
for(let i = 0; i < list.length; i++) {
|
||||
list.item(i).removeAttribute('data-active');
|
||||
}
|
||||
userLine.setAttribute('data-active', 'true');
|
||||
noteContainer.style.opacity = '1';
|
||||
note.innerHTML = Notes.getNote(user);
|
||||
activeNote = user;
|
||||
});
|
||||
userList.appendChild(userLine);
|
||||
}
|
||||
|
||||
noteContainer.appendChild(note);
|
||||
noteContainer.appendChild(saveNote);
|
||||
|
||||
content.appendChild(userList);
|
||||
content.appendChild(noteContainer);
|
||||
|
||||
blurredBackground.appendChild(container);
|
||||
document.body.appendChild(blurredBackground);
|
||||
},
|
||||
importNotesConflictResolve: (user, resultsContainer, container, blurredBackground) => {
|
||||
const diffBlurredBackground = createElement('div', null, 'twitch-notes-blurred-background');
|
||||
const diffContainer = createElement('div', '<strong>Select which note to keep for <em>' + user + '</em></strong><br /><em>You can modify notes to merge them and keep updated one</em><br /><br />', 'twitch-notes-center-floating-container');
|
||||
|
||||
const inputContainer = createElement('div', null, null, {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'no-wrap',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
const valueStyle = {
|
||||
minWidth: '320px',
|
||||
padding: '6px',
|
||||
border: '1px solid #FFFFFF19',
|
||||
outline: 'none',
|
||||
};
|
||||
const valueAttr = {contentEditable: 'true'};
|
||||
|
||||
const localValueContainer = createElement('div');
|
||||
const pLocal = createElement('p', '<strong>Locally saved note</strong>');
|
||||
localValueContainer.appendChild(pLocal);
|
||||
const localValue = createElement('div', Notes.getNote(user), null, valueStyle, valueAttr, 'local-value-container');
|
||||
const localValueSave = createElement('button', 'Save this', 'twitch-notes-settings-button');
|
||||
localValueSave.addEventListener('click', () => {
|
||||
const val = document.getElementById('local-value-container').innerHTML;
|
||||
Notes.inputData.notes = Notes.inputData.notes.map(n => n.user === user ? {
|
||||
user: n.user,
|
||||
note: val,
|
||||
resolved: true
|
||||
} : n);
|
||||
diffBlurredBackground.remove();
|
||||
resultsContainer.remove();
|
||||
Notes.importNotesResult(container, blurredBackground);
|
||||
});
|
||||
localValueContainer.appendChild(localValue);
|
||||
localValueContainer.innerHTML += '<br />';
|
||||
localValueContainer.appendChild(localValueSave);
|
||||
|
||||
const importedValueContainer = createElement('div');
|
||||
const iLocal = createElement('p', '<strong>Imported note</strong>');
|
||||
importedValueContainer.appendChild(iLocal);
|
||||
const importedValue = createElement('div', Notes.inputData.notes.filter(n => n.user === user)[0].note || '', null, valueStyle, valueAttr, 'import-value-container');
|
||||
const importedValueSave = createElement('button', 'Save this', 'twitch-notes-settings-button');
|
||||
importedValueSave.addEventListener('click', () => {
|
||||
const val = document.getElementById('import-value-container').innerHTML;
|
||||
Notes.inputData.notes = Notes.inputData.notes.map(n => n.user === user ? {
|
||||
user: n.user,
|
||||
note: val,
|
||||
resolved: true
|
||||
} : n);
|
||||
diffBlurredBackground.remove();
|
||||
resultsContainer.remove();
|
||||
Notes.importNotesResult(container, blurredBackground);
|
||||
});
|
||||
|
||||
importedValueContainer.appendChild(importedValue);
|
||||
importedValueContainer.innerHTML += '<br />';
|
||||
importedValueContainer.appendChild(importedValueSave);
|
||||
|
||||
|
||||
inputContainer.appendChild(localValueContainer);
|
||||
inputContainer.appendChild(importedValueContainer);
|
||||
|
||||
diffContainer.appendChild(inputContainer);
|
||||
diffBlurredBackground.appendChild(diffContainer);
|
||||
document.body.appendChild(diffBlurredBackground);
|
||||
},
|
||||
importNotesResult: (container, blurredBackground) => {
|
||||
|
||||
const willBeAdded = [];
|
||||
let willBeOverwritten = [];
|
||||
const noChanges = [];
|
||||
|
||||
for (let user of Notes.inputData.users) {
|
||||
if (Notes.getNote(user)) {
|
||||
if (Notes.inputData.notes.find(note => note.user === user).resolved || Notes.getNote(user) !== Notes.inputData.notes.find(note => note.user === user).note) {
|
||||
willBeOverwritten.push(user);
|
||||
} else {
|
||||
noChanges.push(user);
|
||||
}
|
||||
} else {
|
||||
willBeAdded.push(user);
|
||||
}
|
||||
}
|
||||
|
||||
const resultsContainer = createElement('div', null);
|
||||
|
||||
if (willBeOverwritten.length) {
|
||||
const overwrittenContainer = createElement('div', '<strong>Note conflicts found for:</strong><br />', null, {
|
||||
marginTop: '1rem',
|
||||
});
|
||||
for (let item of willBeOverwritten) {
|
||||
const row = createElement('div', item + ' ', null, {
|
||||
color: Notes.inputData.notes.find(note => note.user === item).resolved ? 'green' : 'red',
|
||||
});
|
||||
const resolve = createElement('a', Notes.inputData.notes.find(note => note.user === item).resolved ? '[update]' : '[resolve]', null, {
|
||||
cursor: 'pointer',
|
||||
});
|
||||
resolve.addEventListener('click', () => {
|
||||
Notes.importNotesConflictResolve(item, resultsContainer, container, blurredBackground);
|
||||
})
|
||||
row.appendChild(resolve);
|
||||
overwrittenContainer.appendChild(row);
|
||||
}
|
||||
resultsContainer.appendChild(overwrittenContainer);
|
||||
}
|
||||
|
||||
if (willBeAdded.length) {
|
||||
const addedContainer = createElement('div', '<strong>Note will be added for:</strong><br />' + willBeAdded.join('<br />'), null, {
|
||||
marginTop: '1rem',
|
||||
color: 'white'
|
||||
})
|
||||
resultsContainer.appendChild(addedContainer);
|
||||
}
|
||||
|
||||
if (noChanges.length) {
|
||||
const noChangesContainer = createElement('div', '<strong>No changes for:</strong><br />' + noChanges.join('<br />'), null, {
|
||||
marginTop: '1rem',
|
||||
color: 'gray'
|
||||
})
|
||||
resultsContainer.appendChild(noChangesContainer);
|
||||
}
|
||||
|
||||
const spacer = createElement('div', null, null, {height: '24px'});
|
||||
resultsContainer.appendChild(spacer);
|
||||
|
||||
willBeOverwritten = willBeOverwritten.filter(x => !Notes.inputData.notes.find(n => n.user === x).resolved);
|
||||
|
||||
const saveButton = createElement('button', 'Complete import', 'twitch-notes-settings-button',
|
||||
willBeOverwritten.length > 0 ? {background: 'gray'} : null,
|
||||
willBeOverwritten.length > 0 ? {disabled: 'true'} : null)
|
||||
|
||||
resultsContainer.appendChild(saveButton)
|
||||
|
||||
if (willBeOverwritten.length > 0) {
|
||||
const note = createElement('div', '<em>Import cannot be completed while there are unresolved conflicts</em>', null, {
|
||||
color: 'gray',
|
||||
});
|
||||
resultsContainer.appendChild(note);
|
||||
} else {
|
||||
saveButton.addEventListener('click', () => {
|
||||
for (let note of Notes.inputData.notes) {
|
||||
Notes.saveNote(note.user, note.note);
|
||||
}
|
||||
blurredBackground.remove();
|
||||
Notes.inputData = {};
|
||||
});
|
||||
}
|
||||
|
||||
container.appendChild(resultsContainer);
|
||||
},
|
||||
importNotes: () => {
|
||||
const blurredBackground = createElement('div', null, 'twitch-notes-blurred-background');
|
||||
const container = createElement('div', '<strong>WARNING!</strong> <em>This action might override existing data!</em><br /><br />', 'twitch-notes-center-floating-container');
|
||||
const inputBlock = createElement('div', 'Select exported twitch notes file<br /><br />');
|
||||
const input = createElement('input', null, 'twitch-notes-file-input', null, {
|
||||
type: 'file',
|
||||
accept: '.json'
|
||||
});
|
||||
const settingsCloseButton = createElement('button', xButton, 'twitch-notes-settings-close-button', {
|
||||
position: 'absolute',
|
||||
top: '1rem',
|
||||
right: '1rem',
|
||||
});
|
||||
inputBlock.appendChild(input);
|
||||
settingsCloseButton.addEventListener('click', () => {
|
||||
blurredBackground.remove();
|
||||
});
|
||||
input.addEventListener('change', () => {
|
||||
let file = input.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
let reader = new FileReader();
|
||||
reader.readAsText(file, "UTF-8");
|
||||
reader.onload = function (evt) {
|
||||
Notes.inputData = JSON.parse(evt.target.result.toString());
|
||||
if (Notes.inputData) {
|
||||
inputBlock.style.display = 'none';
|
||||
Notes.importNotesResult(container, blurredBackground);
|
||||
}
|
||||
}
|
||||
reader.onerror = function () {
|
||||
console.error("error reading file");
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(settingsCloseButton);
|
||||
container.appendChild(inputBlock);
|
||||
blurredBackground.appendChild(container);
|
||||
document.body.appendChild(blurredBackground);
|
||||
},
|
||||
clearAllData: () => {
|
||||
const blurredBackground = createElement('div', null, 'twitch-notes-blurred-background');
|
||||
const container = createElement('div', '<strong>This action will delete all saved notes!</strong><br /><br /><em>Consider exporting notes before performing this action in case you will need to use notes again</em>', 'twitch-notes-center-floating-container');
|
||||
|
||||
const spacer = createElement('div', null, null, {
|
||||
height: '24px'
|
||||
});
|
||||
const deleteAction = createElement('button', 'DELETE ALL NOTES', 'twitch-notes-settings-button', {
|
||||
background: 'red'
|
||||
});
|
||||
deleteAction.addEventListener('click', () => {
|
||||
const users = Notes.getSavedUserList();
|
||||
for(let user of users) {
|
||||
Notes.deleteNote(user);
|
||||
}
|
||||
blurredBackground.remove();
|
||||
})
|
||||
const cancelAction = createElement('button', 'Cancel', 'twitch-notes-settings-button');
|
||||
cancelAction.addEventListener('click', () => {
|
||||
blurredBackground.remove();
|
||||
})
|
||||
|
||||
container.appendChild(spacer);
|
||||
container.appendChild(deleteAction);
|
||||
container.appendChild(cancelAction);
|
||||
|
||||
blurredBackground.appendChild(container);
|
||||
document.body.appendChild(blurredBackground);
|
||||
},
|
||||
openAllNotes: () => {
|
||||
Notes.showAllNotes();
|
||||
}
|
||||
}
|
||||
|
||||
const Mouse = {
|
||||
mousePosition: {x: 0, y: 0},
|
||||
lastMousePosition: {x: 0, y: 0},
|
||||
handleMouseMove: (event) => {
|
||||
if (isMouseDown) {
|
||||
if (activeContainer) {
|
||||
const dx = Mouse.mousePosition.x - Mouse.lastMousePosition.x;
|
||||
const dy = Mouse.mousePosition.y - Mouse.lastMousePosition.y;
|
||||
openContainers[activeContainer].style.top =
|
||||
parseInt(openContainers[activeContainer].style.top) + dy + "px";
|
||||
openContainers[activeContainer].style.left =
|
||||
parseInt(openContainers[activeContainer].style.left) + dx + "px";
|
||||
}
|
||||
}
|
||||
Mouse.updateMousePosition(event.clientX, event.clientY);
|
||||
},
|
||||
handleMouseUp: () => {
|
||||
activeContainer = null;
|
||||
isMouseDown = false;
|
||||
},
|
||||
updateMousePosition: (x, y) => {
|
||||
Mouse.lastMousePosition = {...Mouse.mousePosition};
|
||||
Mouse.mousePosition = {x, y};
|
||||
}
|
||||
}
|
||||
|
||||
function createElement(tag, body, classes, styles, attributes, id) {
|
||||
const element = document.createElement(tag);
|
||||
|
||||
if (body) {
|
||||
element.innerHTML = body;
|
||||
}
|
||||
|
||||
if (classes) {
|
||||
if (typeof classes === 'object') {
|
||||
for (let className of classes) {
|
||||
element.classList.add((className));
|
||||
}
|
||||
} else {
|
||||
element.classList.add(classes);
|
||||
}
|
||||
}
|
||||
|
||||
if (styles) {
|
||||
if (typeof styles === 'object') {
|
||||
for (let style in styles) {
|
||||
element.style[style] = styles[style];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attributes) {
|
||||
if (typeof attributes === 'object') {
|
||||
for (let attribute in attributes) {
|
||||
element.setAttribute(attribute, attributes[attribute]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
element.setAttribute('id', id ? id : 'element-' + Math.floor((Math.random() * 10000000)))
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function createSettingsButton(buttonContainer) {
|
||||
|
||||
const c1 = createElement('div', null, null, {marginLeft: "0.5rem !important"});
|
||||
const c2 = createElement('div', null, null, {display: "inline-flex !important"});
|
||||
const settingsButton = createElement('button', 'Notes', 'twitch-notes-settings-button');
|
||||
|
||||
settingsButton.addEventListener("click", toggleSettings);
|
||||
|
||||
c2.appendChild(settingsButton);
|
||||
c1.appendChild(c2);
|
||||
|
||||
buttonContainer.lastChild.insertBefore(
|
||||
c1,
|
||||
buttonContainer.lastChild.lastChild
|
||||
);
|
||||
}
|
||||
|
||||
function removeContainer(username) {
|
||||
if (!openContainers[username]) return;
|
||||
openContainers[username].remove();
|
||||
delete openContainers[username];
|
||||
}
|
||||
|
||||
function addContainer(username, container) {
|
||||
if (openContainers[username]) return;
|
||||
openContainers[username] = container;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
function openTwitchNote(username) {
|
||||
|
||||
const container = createElement('div', null, 'twitch-note-container', {
|
||||
left: Mouse.mousePosition.x + 'px',
|
||||
top: Mouse.mousePosition.y + 10 + 'px'
|
||||
});
|
||||
|
||||
const header = createElement('div', null, 'twitch-note-header');
|
||||
header.addEventListener("mousedown", function close() {
|
||||
activeContainer = username;
|
||||
isMouseDown = true;
|
||||
});
|
||||
|
||||
const closeButton = createElement('span', xButton, 'twitch-note-close-button')
|
||||
closeButton.addEventListener("click", function close() {
|
||||
removeContainer(username);
|
||||
});
|
||||
|
||||
const title = createElement('span', username, 'twitch-note-title');
|
||||
const content = createElement('div', Notes.getNote(username), 'twitch-note-content', [], {
|
||||
contentEditable: 'true'
|
||||
});
|
||||
|
||||
const saveButton = createElement('div', 'SAVE', 'twitch-note-save-button')
|
||||
saveButton.addEventListener("click", function () {
|
||||
Notes.saveNote(username, content.innerHTML);
|
||||
removeContainer(username);
|
||||
});
|
||||
|
||||
header.appendChild(closeButton);
|
||||
header.appendChild(title);
|
||||
container.appendChild(header);
|
||||
container.appendChild(content);
|
||||
container.appendChild(saveButton);
|
||||
|
||||
addContainer(username, container);
|
||||
}
|
||||
|
||||
function addNoteBadges() {
|
||||
let nodes = document.getElementsByClassName("chat-line__message");
|
||||
let users = [];
|
||||
for (let node of nodes) {
|
||||
const n = node.querySelector(".chat-author__display-name");
|
||||
if (!n) continue;
|
||||
const usernameContainer = node.querySelector(
|
||||
".chat-line__username-container"
|
||||
);
|
||||
|
||||
const username = n.attributes["data-a-user"].value;
|
||||
if (username) {
|
||||
let noteButton = usernameContainer.querySelector(".twitch-note");
|
||||
if (!noteButton) {
|
||||
|
||||
const twitchNote = createElement('span', null, 'twitch-note', {
|
||||
cursor: 'pointer',
|
||||
})
|
||||
twitchNote.addEventListener("click", () => {
|
||||
openTwitchNote(username);
|
||||
});
|
||||
|
||||
const img = createElement('img', null, null, {
|
||||
height: '18px',
|
||||
paddingRight: '4px'
|
||||
}, {
|
||||
src: "https://cdn.rdarius.lt/icons/32-id-card.png"
|
||||
})
|
||||
|
||||
twitchNote.appendChild(img);
|
||||
|
||||
usernameContainer.insertBefore(
|
||||
twitchNote,
|
||||
usernameContainer.firstChild
|
||||
);
|
||||
}
|
||||
if (!users[username]) {
|
||||
users[username] = 0;
|
||||
}
|
||||
users[username]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createChatSettingsLine(text) {
|
||||
const element = createElement('div', null, 'twitch-notes-settings-option-line');
|
||||
const button = createElement('button', null, 'twitch-notes-settings-option-line-button');
|
||||
const buttonContainer = createElement('div', null, 'twitch-notes-settings-option-line-button-container');
|
||||
const buttonContainerContent = createElement('div', text, 'twitch-notes-settings-option-line-button-container-content');
|
||||
buttonContainer.appendChild(buttonContainerContent);
|
||||
button.appendChild(buttonContainer);
|
||||
element.appendChild(button);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function createChatSettingsSeparator() {
|
||||
return createElement('div', null, 'twitch-note-settings-separator');
|
||||
}
|
||||
|
||||
function toggleSettings() {
|
||||
if (!settingsWindow) {
|
||||
const settingsContainer = createElement('div', null, 'twitch-notes-settings-container');
|
||||
const settingsBalloon = createElement('div', null, 'twitch-notes-settings-balloon');
|
||||
const settingsPopover = createElement('div', null, 'twitch-notes-settings-popover');
|
||||
|
||||
// startOf: settings header
|
||||
const settingsHeader = createElement('div', null, 'twitch-notes-settings-header');
|
||||
const settingsHeaderLeftElement = createElement('div', null, 'twitch-notes-settings-header-left-element')
|
||||
settingsHeader.appendChild(settingsHeaderLeftElement);
|
||||
const settingsHeaderCenterElement = createElement('div', null, 'twitch-notes-settings-header-center-element')
|
||||
settingsHeader.appendChild(settingsHeaderCenterElement);
|
||||
const settingsHeaderCenterElementContent = createElement('p', 'Twitch Notes Settings', 'twitch-notes-settings-header-center-element-content')
|
||||
settingsHeaderCenterElement.appendChild(settingsHeaderCenterElementContent);
|
||||
const settingsHeaderRightElement = createElement('div', null, 'twitch-notes-settings-header-right-element')
|
||||
settingsHeader.appendChild(settingsHeaderRightElement);
|
||||
const settingsCloseButton = createElement('button', xButton, 'twitch-notes-settings-close-button');
|
||||
settingsHeaderRightElement.appendChild(settingsCloseButton);
|
||||
settingsPopover.appendChild(settingsHeader);
|
||||
// endOf: settings header
|
||||
|
||||
settingsCloseButton.addEventListener('click', toggleSettings);
|
||||
|
||||
|
||||
// startOf: scrollable area
|
||||
const settingsScrollableArea = createElement('div', null, 'twitch-notes-settings-scrollable-area');
|
||||
const settingsContent = createElement('div', null, 'twitch-notes-settings-content');
|
||||
settingsScrollableArea.appendChild(settingsContent);
|
||||
|
||||
const exportNotes = createChatSettingsLine('Export Notes');
|
||||
settingsContent.appendChild(exportNotes);
|
||||
const importNotes = createChatSettingsLine('Import Notes');
|
||||
settingsContent.appendChild(importNotes);
|
||||
const clearData = createChatSettingsLine('Clear Data');
|
||||
settingsContent.appendChild(clearData);
|
||||
settingsContent.appendChild(createChatSettingsSeparator());
|
||||
const openAllNotes = createChatSettingsLine('View All Notes');
|
||||
settingsContent.appendChild(openAllNotes);
|
||||
|
||||
exportNotes.addEventListener('click', () => {Notes.exportNotes(); toggleSettings()});
|
||||
importNotes.addEventListener('click', () => {Notes.importNotes(); toggleSettings()});
|
||||
clearData.addEventListener('click', () => {Notes.clearAllData(); toggleSettings()});
|
||||
openAllNotes.addEventListener('click', () => {Notes.openAllNotes(); toggleSettings()});
|
||||
|
||||
|
||||
settingsPopover.appendChild(settingsScrollableArea);
|
||||
// endOf: scrollable area
|
||||
|
||||
|
||||
settingsBalloon.appendChild(settingsPopover);
|
||||
settingsContainer.appendChild(settingsBalloon);
|
||||
settingsWindow = settingsContainer;
|
||||
document.body.appendChild(settingsWindow);
|
||||
settingsWindowOpen = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (settingsWindowOpen) {
|
||||
settingsWindow.style.display = 'none';
|
||||
settingsWindowOpen = false;
|
||||
} else {
|
||||
settingsWindow.style.display = 'block';
|
||||
settingsWindowOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
const xButton = `<svg width="100%" height="100%" viewBox="0 0 20 20" x="0px" y="0px" class="twitch-notes-x-button"><g><path d="M8.5 10L4 5.5 5.5 4 10 8.5 14.5 4 16 5.5 11.5 10l4.5 4.5-1.5 1.5-4.5-4.5L5.5 16 4 14.5 8.5 10z"></path></g></svg>`;
|
||||
let openContainers = {};
|
||||
let isMouseDown = false;
|
||||
let activeContainer = null;
|
||||
let settingsWindowOpen = false;
|
||||
let settingsWindow = null;
|
||||
|
||||
let LS_UserList = "twitch-note-all-users-list";
|
||||
let LS_Prefix = "twitch-note-";
|
||||
|
||||
document.addEventListener("mousemove", Mouse.handleMouseMove);
|
||||
document.addEventListener("mouseup", Mouse.handleMouseUp);
|
||||
|
||||
let giveUpAt = Date.now() + 20000;
|
||||
|
||||
let changeInProgress = false;
|
||||
|
||||
let targetNode;
|
||||
let buttonContainer;
|
||||
|
||||
do {
|
||||
targetNode = document.getElementsByClassName(
|
||||
"chat-scrollable-area__message-container"
|
||||
)[0];
|
||||
} while (!targetNode || giveUpAt < Date.now());
|
||||
|
||||
do {
|
||||
buttonContainer = document.getElementsByClassName(
|
||||
"chat-input__buttons-container"
|
||||
)[0];
|
||||
} while (!buttonContainer || giveUpAt < Date.now());
|
||||
|
||||
createSettingsButton(buttonContainer);
|
||||
|
||||
// Options for the observer (which mutations to observe)
|
||||
const config = {attributes: true, childList: true, subtree: true};
|
||||
|
||||
// Callback function to execute when mutations are observed
|
||||
const callback = (mutationList) => {
|
||||
if (!changeInProgress) {
|
||||
for (const mutation of mutationList) {
|
||||
if (mutation.type === "childList") {
|
||||
changeInProgress = true;
|
||||
addNoteBadges();
|
||||
changeInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create an observer instance linked to the callback function
|
||||
const observer = new MutationObserver(callback);
|
||||
|
||||
// Start observing the target node for configured mutations
|
||||
observer.observe(targetNode, config);
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
168
src/CustomHTMLElement.ts
Normal file
168
src/CustomHTMLElement.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import {CustomHTMLElementConstructor} from "./types";
|
||||
|
||||
export class CustomHTMLElement<T = HTMLElement> {
|
||||
|
||||
element: HTMLElement;
|
||||
|
||||
constructor(element: string, data: CustomHTMLElementConstructor) {
|
||||
this.element = document.createElement(element);
|
||||
|
||||
if (data.id) {
|
||||
this.setId(data.id);
|
||||
} else {
|
||||
this.setId(element + '-generated-id-' + Math.floor(Math.random() * 10000000));
|
||||
}
|
||||
|
||||
if (data.class) {
|
||||
if (typeof data.class === "string") {
|
||||
this.addClass(data.class);
|
||||
} else {
|
||||
for (const className of data.class) {
|
||||
this.addClass(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.attributes) {
|
||||
for (const key in data.attributes) {
|
||||
this.addAttribute(key, data.attributes[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.css) this.setStyle(data.css);
|
||||
|
||||
if (data.body) typeof data.body === "string" ? this.setBody(data.body) : this.appendChild(data.body);
|
||||
if (data.customBody) this.appendCustomChild(data.customBody);
|
||||
}
|
||||
|
||||
setId(id: string): CustomHTMLElement<T> {
|
||||
this.element.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
addClass(classes: string): CustomHTMLElement<T> {
|
||||
this.element.classList.add(classes);
|
||||
return this;
|
||||
}
|
||||
|
||||
addAttribute(name: string, value: string): CustomHTMLElement<T> {
|
||||
this.element.setAttribute(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
setStyle(attributes: Partial<CSSStyleDeclaration>): CustomHTMLElement<T> {
|
||||
for(let key in attributes) {
|
||||
this.element.style[key] = attributes[key] || '';
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
setBody(body: string): CustomHTMLElement<T> {
|
||||
this.element.innerHTML = body;
|
||||
return this;
|
||||
}
|
||||
|
||||
appendBody(body: string): CustomHTMLElement<T> {
|
||||
this.element.innerHTML += body;
|
||||
return this;
|
||||
}
|
||||
|
||||
appendChild(child: HTMLElement): CustomHTMLElement<T> {
|
||||
this.element.appendChild(child);
|
||||
return this;
|
||||
}
|
||||
|
||||
appendCustomChild(child: CustomHTMLElement): CustomHTMLElement<T> {
|
||||
this.element.appendChild(child.getElement());
|
||||
return this;
|
||||
}
|
||||
|
||||
getElement(): T {
|
||||
return this.element as T;
|
||||
}
|
||||
|
||||
setClickListener(listener: Function): CustomHTMLElement<T> {
|
||||
this.element.addEventListener('click', () => listener());
|
||||
return this;
|
||||
}
|
||||
|
||||
setChangeListener(listener: Function): CustomHTMLElement<T> {
|
||||
this.element.addEventListener('change', () => listener());
|
||||
return this;
|
||||
}
|
||||
|
||||
setMouseMoveListener(listener: Function): CustomHTMLElement<T> {
|
||||
this.element.addEventListener('mousemove', () => listener());
|
||||
return this;
|
||||
}
|
||||
|
||||
setMouseDownListener(listener: Function): CustomHTMLElement<T> {
|
||||
this.element.addEventListener('mousedown', () => listener());
|
||||
return this;
|
||||
}
|
||||
|
||||
setMouseUpListener(listener: Function): CustomHTMLElement<T> {
|
||||
this.element.addEventListener('mouseup', () => listener());
|
||||
return this;
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
show(): CustomHTMLElement<T> {
|
||||
this.setStyle({
|
||||
display: 'unset',
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
hide(): CustomHTMLElement<T> {
|
||||
this.setStyle({
|
||||
display: 'none',
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CustomDivElement extends CustomHTMLElement<HTMLDivElement> {
|
||||
constructor(className: string = '', content: string = '') {
|
||||
super('div', {class: className, body: content});
|
||||
}
|
||||
}
|
||||
export class CustomInputElement extends CustomHTMLElement<HTMLInputElement> {
|
||||
constructor(className: string = '') {
|
||||
super('input', {class: className});
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomPElement extends CustomHTMLElement<HTMLParagraphElement> {
|
||||
constructor(className: string = '', content: string = '') {
|
||||
super('p', {class: className, body: content});
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomButtonElement extends CustomHTMLElement<HTMLButtonElement> {
|
||||
constructor(className: string = '', content: string = '') {
|
||||
super('button', {class: className, body: content});
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomAElement extends CustomHTMLElement<HTMLAnchorElement> {
|
||||
constructor(className: string = '', content: string = '') {
|
||||
super('a', {class: className, body: content});
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomSpanElement extends CustomHTMLElement<HTMLSpanElement> {
|
||||
constructor(className: string = '', content: string = '') {
|
||||
super('span', {class: className, body: content});
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomImgElement extends CustomHTMLElement<HTMLSpanElement> {
|
||||
constructor(className: string = '', content: string = '') {
|
||||
super('img', {class: className, body: content});
|
||||
}
|
||||
}
|
||||
509
src/HTMLTemplates.ts
Normal file
509
src/HTMLTemplates.ts
Normal file
@@ -0,0 +1,509 @@
|
||||
import {
|
||||
CustomAElement,
|
||||
CustomButtonElement,
|
||||
CustomDivElement,
|
||||
CustomHTMLElement,
|
||||
CustomInputElement,
|
||||
CustomPElement, CustomSpanElement
|
||||
} from "./CustomHTMLElement";
|
||||
import NoteStorage from "./NoteStorage";
|
||||
import Mouse from "./Mouse";
|
||||
import NoteContainers from "./NoteContainers";
|
||||
|
||||
export function toggleSettingsList() {
|
||||
const settingsWindow = document.querySelector('.twitch-notes-settings-container') as HTMLElement;
|
||||
if (!settingsWindow) {
|
||||
const settingsContainer = new CustomDivElement('twitch-notes-settings-container');
|
||||
const settingsBalloon = new CustomDivElement('twitch-notes-settings-balloon');
|
||||
const settingsPopover = new CustomDivElement('twitch-notes-settings-popover');
|
||||
const settingsHeader = new CustomDivElement('twitch-notes-settings-header');
|
||||
const settingsHeaderLeftElement = new CustomDivElement('twitch-notes-settings-header-left-element');
|
||||
const settingsHeaderCenterElement = new CustomDivElement('twitch-notes-settings-header-center-element');
|
||||
const settingsHeaderCenterElementContent = new CustomPElement('twitch-notes-settings-header-center-element-content','Twitch Notes Settings');
|
||||
const settingsHeaderRightElement = new CustomDivElement('twitch-notes-settings-header-right-element');
|
||||
const settingsCloseButton = new CustomButtonElement('twitch-notes-settings-close-button', getCloseButtonSVG());
|
||||
const settingsScrollableArea = new CustomDivElement('twitch-notes-settings-scrollable-area');
|
||||
const settingsContent = new CustomDivElement('twitch-notes-settings-content');
|
||||
const exportNotes = createChatSettingsLine('Export Notes');
|
||||
const importNotes = createChatSettingsLine('Import Notes');
|
||||
const clearData = createChatSettingsLine('Clear Data');
|
||||
const openAllNotes = createChatSettingsLine('View All Notes');
|
||||
|
||||
settingsContainer.appendCustomChild(settingsBalloon);
|
||||
settingsBalloon.appendCustomChild(settingsPopover);
|
||||
settingsPopover.appendCustomChild(settingsHeader);
|
||||
settingsHeader.appendCustomChild(settingsHeaderLeftElement);
|
||||
settingsHeader.appendCustomChild(settingsHeaderCenterElement);
|
||||
settingsHeaderCenterElement.appendCustomChild(settingsHeaderCenterElementContent);
|
||||
settingsHeader.appendCustomChild(settingsHeaderRightElement);
|
||||
settingsHeaderRightElement.appendCustomChild(settingsCloseButton);
|
||||
settingsPopover.appendCustomChild(settingsScrollableArea);
|
||||
settingsScrollableArea.appendCustomChild(settingsContent);
|
||||
settingsContent.appendCustomChild(exportNotes);
|
||||
settingsContent.appendCustomChild(importNotes);
|
||||
settingsContent.appendCustomChild(clearData);
|
||||
settingsContent.appendCustomChild(createChatSettingsSeparator());
|
||||
settingsContent.appendCustomChild(openAllNotes);
|
||||
|
||||
settingsCloseButton.setClickListener(() => toggleSettingsList());
|
||||
exportNotes.setClickListener(() => {
|
||||
NoteStorage.exportNotes();
|
||||
toggleSettingsList();
|
||||
});
|
||||
importNotes.setClickListener(() => {
|
||||
openImportNotesWindow();
|
||||
toggleSettingsList();
|
||||
});
|
||||
clearData.setClickListener(() => {
|
||||
openClearDataWindow();
|
||||
toggleSettingsList();
|
||||
});
|
||||
openAllNotes.setClickListener(() => {
|
||||
openAllNoteListWindow();
|
||||
toggleSettingsList();
|
||||
});
|
||||
|
||||
document.body.appendChild(settingsContainer.getElement());
|
||||
return;
|
||||
} else {
|
||||
settingsWindow.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export function openAllNoteListWindow() {
|
||||
let activeNote: string = '';
|
||||
const blurredBackground = new CustomDivElement('twitch-notes-blurred-background');
|
||||
|
||||
const noteSaved = new CustomSpanElement('', 'Note saved!');
|
||||
noteSaved.setStyle({
|
||||
color: 'green',
|
||||
marginLeft: '2rem',
|
||||
});
|
||||
noteSaved.hide();
|
||||
|
||||
const container = new CustomDivElement('twitch-notes-center-floating-container');
|
||||
container.setStyle({
|
||||
padding: '0',
|
||||
paddingTop: '1rem',
|
||||
width: '100%',
|
||||
maxWidth: '800px',
|
||||
height: 'calc(320px + 5rem)',
|
||||
maxHeight: '90vh',
|
||||
});
|
||||
|
||||
const closeButton = new CustomButtonElement('twitch-notes-settings-close-button', getCloseButtonSVG());
|
||||
closeButton.setStyle({
|
||||
position: 'absolute',
|
||||
top: '1rem',
|
||||
right: '1rem',
|
||||
});
|
||||
|
||||
const title = new CustomDivElement('twitch-note-title', 'Notes:');
|
||||
title.setStyle({
|
||||
borderBottom: '1px solid #FFFFFF19',
|
||||
paddingBottom: '1rem',
|
||||
});
|
||||
container.appendCustomChild(title);
|
||||
|
||||
const content = new CustomDivElement();
|
||||
content.setStyle({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'no-wrap',
|
||||
width: '100%',
|
||||
height: '320px'
|
||||
});
|
||||
container.appendCustomChild(content);
|
||||
closeButton.setClickListener(() => {
|
||||
blurredBackground.remove();
|
||||
});
|
||||
container.appendCustomChild(closeButton);
|
||||
|
||||
const userList = new CustomDivElement();
|
||||
userList.setStyle({
|
||||
borderRight: '1px solid #FFFFFF19',
|
||||
height: '320px',
|
||||
minWidth: '240px',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
});
|
||||
|
||||
const noteContainer = new CustomDivElement();
|
||||
noteContainer.setStyle({
|
||||
height: '320px',
|
||||
padding: '1rem',
|
||||
width: '100%',
|
||||
opacity: '0',
|
||||
});
|
||||
|
||||
const note = new CustomDivElement();
|
||||
note.setStyle({
|
||||
marginBottom: '3rem',
|
||||
border: '1px solid #FFFFFF19',
|
||||
height: 'calc(320px - 7rem)',
|
||||
width: '100%',
|
||||
padding: '6px',
|
||||
outline: 'none',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
});
|
||||
note.addAttribute('contentEditable', 'true');
|
||||
|
||||
const saveNote = new CustomButtonElement('twitch-notes-settings-button','Save Note');
|
||||
saveNote.setStyle({
|
||||
position: 'absolute',
|
||||
bottom: '1rem',
|
||||
left: '1rem'
|
||||
});
|
||||
|
||||
saveNote.setClickListener(() => {
|
||||
if(!activeNote) {
|
||||
return;
|
||||
}
|
||||
NoteStorage.saveNote(activeNote, note.getElement().innerHTML);
|
||||
noteSaved.setStyle({
|
||||
display: 'inline-block',
|
||||
});
|
||||
});
|
||||
|
||||
for(let user of NoteStorage.getSavedUserList()) {
|
||||
const userLine = new CustomDivElement('twitch-notes-user-list-user', user);
|
||||
userLine.setClickListener(() => {
|
||||
const list = document.getElementsByClassName('twitch-notes-user-list-user');
|
||||
for(let i = 0; i < list.length; i++) {
|
||||
list.item(i)?.removeAttribute('data-active');
|
||||
}
|
||||
noteSaved.hide();
|
||||
userLine.addAttribute('data-active', 'true');
|
||||
noteContainer.setStyle({opacity: '1'});
|
||||
note.getElement().innerHTML = NoteStorage.getNote(user);
|
||||
activeNote = user;
|
||||
});
|
||||
userList.appendCustomChild(userLine);
|
||||
}
|
||||
|
||||
noteContainer.appendCustomChild(note);
|
||||
noteContainer.appendCustomChild(saveNote);
|
||||
noteContainer.appendCustomChild(noteSaved);
|
||||
|
||||
content.appendCustomChild(userList);
|
||||
content.appendCustomChild(noteContainer);
|
||||
|
||||
blurredBackground.appendCustomChild(container);
|
||||
document.body.appendChild(blurredBackground.getElement());
|
||||
}
|
||||
|
||||
export function openClearDataWindow() {
|
||||
const blurredBackground = new CustomDivElement( 'twitch-notes-blurred-background');
|
||||
const container = new CustomDivElement('twitch-notes-center-floating-container', '<strong>This action will delete all saved notes!</strong><br /><br /><em>Consider exporting notes before performing this action in case you will need to use notes again</em>');
|
||||
const spacer = new CustomDivElement();
|
||||
spacer.setStyle({
|
||||
height: '24px'
|
||||
});
|
||||
const deleteAction = new CustomButtonElement('twitch-notes-settings-button', 'DELETE ALL NOTES');
|
||||
deleteAction.setStyle({
|
||||
background: 'red'
|
||||
});
|
||||
deleteAction.setClickListener(() => {
|
||||
NoteStorage.clearData();
|
||||
blurredBackground.remove();
|
||||
});
|
||||
const cancelAction = new CustomButtonElement('twitch-notes-settings-button', 'Cancel');
|
||||
cancelAction.setClickListener(() => {
|
||||
blurredBackground.remove();
|
||||
});
|
||||
container.appendCustomChild(spacer);
|
||||
container.appendCustomChild(deleteAction);
|
||||
container.appendCustomChild(cancelAction);
|
||||
blurredBackground.appendCustomChild(container);
|
||||
document.body.appendChild(blurredBackground.getElement());
|
||||
}
|
||||
|
||||
export function openImportNotesWindow() {
|
||||
const blurredBackground = new CustomDivElement('twitch-notes-blurred-background');
|
||||
const container = new CustomDivElement('twitch-notes-center-floating-container', '<strong>WARNING!</strong> <em>This action might override existing data!</em><br /><br />');
|
||||
const inputBlock = new CustomDivElement('','Select exported twitch notes file<br /><br />');
|
||||
const input = new CustomInputElement('twitch-notes-file-input');
|
||||
input.addAttribute('type', 'file').addAttribute('accept', 'json');
|
||||
const settingsCloseButton = new CustomButtonElement('twitch-notes-settings-close-button', getCloseButtonSVG());
|
||||
settingsCloseButton.setStyle({
|
||||
position: 'absolute',
|
||||
top: '1rem',
|
||||
right: '1rem',
|
||||
});
|
||||
|
||||
inputBlock.appendCustomChild(input);
|
||||
|
||||
settingsCloseButton.setClickListener(() => {
|
||||
blurredBackground.remove();
|
||||
})
|
||||
|
||||
input.setChangeListener(() => {
|
||||
if (!input?.getElement()?.files) return;
|
||||
// @ts-ignore
|
||||
let file = input.getElement().files.item(0);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
let reader = new FileReader();
|
||||
reader.readAsText(file, "UTF-8");
|
||||
reader.onload = function (evt) {
|
||||
if (!evt?.target?.result) return;
|
||||
|
||||
NoteStorage.inputData = JSON.parse(evt.target.result.toString());
|
||||
if (NoteStorage.inputData) {
|
||||
inputBlock.setStyle({display: 'none'});
|
||||
importNotesResult(container, blurredBackground);
|
||||
}
|
||||
}
|
||||
reader.onerror = function () {
|
||||
console.error("error reading file");
|
||||
}
|
||||
});
|
||||
|
||||
container.appendCustomChild(settingsCloseButton);
|
||||
container.appendCustomChild(inputBlock);
|
||||
blurredBackground.appendCustomChild(container);
|
||||
document.body.appendChild(blurredBackground.getElement());
|
||||
}
|
||||
|
||||
export function importNotesResult(container: CustomHTMLElement, blurredBackground: CustomHTMLElement) {
|
||||
const willBeAdded = [];
|
||||
let willBeOverwritten = [];
|
||||
const noChanges = [];
|
||||
|
||||
for (let user of NoteStorage.inputData.users) {
|
||||
if (NoteStorage.getNote(user)) {
|
||||
if (NoteStorage.inputData.notes.find(note => note.user === user)?.resolved || NoteStorage.getNote(user) !== NoteStorage.inputData.notes.find(note => note.user === user)?.note) {
|
||||
willBeOverwritten.push(user);
|
||||
} else {
|
||||
noChanges.push(user);
|
||||
}
|
||||
} else {
|
||||
willBeAdded.push(user);
|
||||
}
|
||||
}
|
||||
|
||||
const resultsContainer = new CustomDivElement();
|
||||
|
||||
if (willBeOverwritten.length) {
|
||||
const overwrittenContainer = new CustomDivElement('', '<strong>Note conflicts found for:</strong><br />');
|
||||
overwrittenContainer.setStyle({marginTop: '1rem'});
|
||||
|
||||
for (let item of willBeOverwritten) {
|
||||
const row = new CustomDivElement('', item + ' ');
|
||||
const isResolved = NoteStorage.inputData.notes.find(note => note.user === item)?.resolved;
|
||||
row.setStyle({
|
||||
color: isResolved ? 'green' : 'red',
|
||||
});
|
||||
const resolve = new CustomAElement('', isResolved ? '[update]' : '[resolve]');
|
||||
resolve.setStyle({
|
||||
cursor: 'pointer'
|
||||
});
|
||||
|
||||
resolve.setClickListener(() => {
|
||||
importNotesConflictResolve(item, resultsContainer, container, blurredBackground);
|
||||
})
|
||||
row.appendCustomChild(resolve);
|
||||
overwrittenContainer.appendCustomChild(row);
|
||||
}
|
||||
resultsContainer.appendCustomChild(overwrittenContainer);
|
||||
}
|
||||
|
||||
if (willBeAdded.length) {
|
||||
const addedContainer = new CustomDivElement('', '<strong>Note will be added for:</strong><br />' + willBeAdded.join('<br />'));
|
||||
addedContainer.setStyle({
|
||||
marginTop: '1rem',
|
||||
color: 'white'
|
||||
});
|
||||
resultsContainer.appendCustomChild(addedContainer);
|
||||
}
|
||||
|
||||
if (noChanges.length) {
|
||||
const noChangesContainer = new CustomDivElement('', '<strong>No changes for:</strong><br />' + noChanges.join('<br />'));
|
||||
noChangesContainer.setStyle({
|
||||
marginTop: '1rem',
|
||||
color: 'gray'
|
||||
});
|
||||
resultsContainer.appendCustomChild(noChangesContainer);
|
||||
}
|
||||
|
||||
const spacer = new CustomDivElement();
|
||||
spacer.setStyle({height: '24px'});
|
||||
resultsContainer.appendCustomChild(spacer);
|
||||
|
||||
willBeOverwritten = willBeOverwritten.filter(x => !NoteStorage.inputData.notes.find(n => n.user === x)?.resolved);
|
||||
|
||||
const saveButton = new CustomButtonElement('twitch-notes-settings-button', 'Complete import');
|
||||
saveButton.setStyle({})
|
||||
if (willBeOverwritten.length > 0) saveButton.setStyle({background: 'gray'});
|
||||
if (willBeOverwritten.length > 0) saveButton.addAttribute('disabled', 'true');
|
||||
|
||||
resultsContainer.appendCustomChild(saveButton)
|
||||
|
||||
if (willBeOverwritten.length > 0) {
|
||||
const note = new CustomDivElement('', '<em>Import cannot be completed while there are unresolved conflicts</em>');
|
||||
note.setStyle({
|
||||
color: 'gray',
|
||||
});
|
||||
resultsContainer.appendCustomChild(note);
|
||||
} else {
|
||||
saveButton.setClickListener(() => {
|
||||
for (let note of NoteStorage.inputData.notes) {
|
||||
NoteStorage.saveNote(note.user, note.note);
|
||||
}
|
||||
blurredBackground.remove();
|
||||
NoteStorage.inputData = {
|
||||
users: [],
|
||||
notes: [],
|
||||
settings: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
container.appendCustomChild(resultsContainer);
|
||||
}
|
||||
|
||||
export function importNotesConflictResolve(
|
||||
user: string,
|
||||
resultsContainer: CustomHTMLElement,
|
||||
container: CustomHTMLElement,
|
||||
blurredBackground: CustomHTMLElement
|
||||
) {
|
||||
const diffBlurredBackground = new CustomDivElement('twitch-notes-blurred-background');
|
||||
const diffContainer = new CustomDivElement('twitch-notes-center-floating-container', '<strong>Select which note to keep for <em>' + user + '</em></strong><br /><em>You can modify notes to merge them and keep updated one</em><br /><br />');
|
||||
|
||||
const inputContainer = new CustomDivElement();
|
||||
inputContainer.setStyle({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'no-wrap',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
const valueStyle = {
|
||||
minWidth: '320px',
|
||||
padding: '6px',
|
||||
border: '1px solid #FFFFFF19',
|
||||
outline: 'none',
|
||||
};
|
||||
const valueAttr: [string, string] = ['contentEditable', 'true'];
|
||||
|
||||
const localValueContainer = new CustomDivElement();
|
||||
const pLocal = new CustomPElement('', '<strong>Locally saved note</strong>');
|
||||
localValueContainer.appendCustomChild(pLocal);
|
||||
|
||||
const localValue = new CustomDivElement();
|
||||
localValue.setStyle(valueStyle);
|
||||
localValue.addAttribute(...valueAttr);
|
||||
localValue.setId('local-value-container');
|
||||
const localValueSave = new CustomButtonElement('twitch-notes-settings-button', 'Save this');
|
||||
localValueSave.setClickListener(() => {
|
||||
const val = document.getElementById('local-value-container')?.innerHTML || '';
|
||||
NoteStorage.inputData.notes = NoteStorage.inputData.notes.map(n => n.user === user ? {
|
||||
user: n.user,
|
||||
note: val,
|
||||
resolved: true
|
||||
} : n);
|
||||
diffBlurredBackground.remove();
|
||||
resultsContainer.remove();
|
||||
importNotesResult(container, blurredBackground);
|
||||
});
|
||||
localValueContainer.appendCustomChild(localValue);
|
||||
localValueContainer.appendBody('<br />');
|
||||
localValueContainer.appendCustomChild(localValueSave);
|
||||
|
||||
const importedValueContainer = new CustomDivElement();
|
||||
const iLocal = new CustomPElement('', '<strong>Imported note</strong>');
|
||||
importedValueContainer.appendCustomChild(iLocal);
|
||||
const importedValue = new CustomDivElement('', NoteStorage.inputData.notes.filter(n => n.user === user)[0].note || '');
|
||||
importedValue.setId('import-value-container');
|
||||
importedValue.setStyle(valueStyle);
|
||||
importedValue.addAttribute(...valueAttr);
|
||||
const importedValueSave = new CustomButtonElement('twitch-notes-settings-button', 'Save this');
|
||||
importedValueSave.setClickListener(() => {
|
||||
const val = document.getElementById('import-value-container')?.innerHTML || '';
|
||||
NoteStorage.inputData.notes = NoteStorage.inputData.notes.map(n => n.user === user ? {
|
||||
user: n.user,
|
||||
note: val,
|
||||
resolved: true
|
||||
} : n);
|
||||
diffBlurredBackground.remove();
|
||||
resultsContainer.remove();
|
||||
importNotesResult(container, blurredBackground);
|
||||
});
|
||||
|
||||
importedValueContainer.appendCustomChild(importedValue);
|
||||
importedValueContainer.appendBody('<br />');
|
||||
importedValueContainer.appendCustomChild(importedValueSave);
|
||||
|
||||
|
||||
inputContainer.appendCustomChild(localValueContainer);
|
||||
inputContainer.appendCustomChild(importedValueContainer);
|
||||
|
||||
diffContainer.appendCustomChild(inputContainer);
|
||||
diffBlurredBackground.appendCustomChild(diffContainer);
|
||||
document.body.appendChild(diffBlurredBackground.getElement());
|
||||
}
|
||||
|
||||
export function createChatSettingsLine(text: string) {
|
||||
const element = new CustomDivElement('twitch-notes-settings-option-line');
|
||||
const button = new CustomButtonElement('twitch-notes-settings-option-line-button');
|
||||
const buttonContainer = new CustomDivElement('twitch-notes-settings-option-line-button-container');
|
||||
const buttonContainerContent = new CustomDivElement('twitch-notes-settings-option-line-button-container-content', text);
|
||||
|
||||
buttonContainer.appendCustomChild(buttonContainerContent);
|
||||
button.appendCustomChild(buttonContainer);
|
||||
element.appendCustomChild(button);
|
||||
return element;
|
||||
}
|
||||
|
||||
export function createChatSettingsSeparator() {
|
||||
return new CustomDivElement('twitch-note-settings-separator');
|
||||
}
|
||||
|
||||
export function openTwitchNote(username: string) {
|
||||
if (NoteContainers.containers[username]) return;
|
||||
const container = new CustomDivElement('twitch-note-container');
|
||||
container.setStyle({
|
||||
left: Mouse.mousePosition.x + 'px',
|
||||
top: Mouse.mousePosition.y + 10 + 'px'
|
||||
});
|
||||
|
||||
const header = new CustomDivElement('twitch-note-header');
|
||||
header.setMouseDownListener(() => {
|
||||
NoteContainers.activeContainer = username;
|
||||
Mouse.isMouseDown = true;
|
||||
});
|
||||
|
||||
const closeButton = new CustomSpanElement('twitch-note-close-button', getCloseButtonSVG());
|
||||
closeButton.setClickListener(() => {
|
||||
NoteContainers.removeContainer(username);
|
||||
});
|
||||
|
||||
const title = new CustomSpanElement('twitch-note-title', username);
|
||||
const content = new CustomDivElement('twitch-note-content', NoteStorage.getNote(username));
|
||||
content.addAttribute('contentEditable', 'true');
|
||||
|
||||
const saveButton = new CustomDivElement('twitch-note-save-button', 'SAVE');
|
||||
saveButton.setClickListener(() => {
|
||||
NoteStorage.saveNote(username, content.getElement().innerHTML);
|
||||
NoteContainers.removeContainer(username);
|
||||
});
|
||||
|
||||
header.appendCustomChild(closeButton);
|
||||
header.appendCustomChild(title);
|
||||
container.appendCustomChild(header);
|
||||
container.appendCustomChild(content);
|
||||
container.appendCustomChild(saveButton);
|
||||
document.body.appendChild(container.getElement());
|
||||
NoteContainers.addContainer(username, container);
|
||||
}
|
||||
|
||||
export function getCloseButtonSVG() {
|
||||
return `<svg width="100%" height="100%" viewBox="0 0 20 20" x="0px" y="0px" class="twitch-notes-x-button">
|
||||
<g>
|
||||
<path d="M8.5 10L4 5.5 5.5 4 10 8.5 14.5 4 16 5.5 11.5 10l4.5 4.5-1.5 1.5-4.5-4.5L5.5 16 4 14.5 8.5 10z"></path>
|
||||
</g>
|
||||
</svg>`;
|
||||
}
|
||||
43
src/Mouse.ts
Normal file
43
src/Mouse.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import NoteContainers from "./NoteContainers";
|
||||
|
||||
|
||||
|
||||
export default class Mouse {
|
||||
|
||||
static isMouseDown = false;
|
||||
static mousePosition = {x: 0, y: 0};
|
||||
static lastMousePosition = {x: 0, y: 0};
|
||||
static listenersSetUp = false;
|
||||
|
||||
static setupListeners() {
|
||||
if (!Mouse.listenersSetUp) {
|
||||
document.addEventListener("mousemove", Mouse.handleMouseMove);
|
||||
document.addEventListener("mouseup", Mouse.handleMouseUp);
|
||||
Mouse.listenersSetUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
static handleMouseUp() {
|
||||
NoteContainers.activeContainer = '';
|
||||
Mouse.isMouseDown = false;
|
||||
}
|
||||
|
||||
static updateMousePosition(x: number, y: number) {
|
||||
Mouse.lastMousePosition = {...this.mousePosition};
|
||||
Mouse.mousePosition = {x, y};
|
||||
}
|
||||
|
||||
static handleMouseMove(event: MouseEvent) {
|
||||
if (Mouse.isMouseDown) {
|
||||
if (NoteContainers.activeContainer) {
|
||||
const dx = Mouse.mousePosition.x - Mouse.lastMousePosition.x;
|
||||
const dy = Mouse.mousePosition.y - Mouse.lastMousePosition.y;
|
||||
NoteContainers.containers[NoteContainers.activeContainer].setStyle({
|
||||
top: parseInt(NoteContainers.containers[NoteContainers.activeContainer].getElement().style.top) + dy + "px",
|
||||
left: parseInt(NoteContainers.containers[NoteContainers.activeContainer].getElement().style.left) + dx + "px",
|
||||
});
|
||||
}
|
||||
}
|
||||
Mouse.updateMousePosition(event.clientX, event.clientY);
|
||||
}
|
||||
};
|
||||
17
src/NoteContainers.ts
Normal file
17
src/NoteContainers.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {CustomHTMLElement} from "./CustomHTMLElement";
|
||||
|
||||
const containers: {[key: string]: CustomHTMLElement} = {};
|
||||
|
||||
export default {
|
||||
activeContainer: '',
|
||||
containers,
|
||||
addContainer(username: string, container: CustomHTMLElement) {
|
||||
if (this.containers[username]) return;
|
||||
this.containers[username] = container;
|
||||
},
|
||||
removeContainer(username: string) {
|
||||
if (!this.containers[username]) return;
|
||||
this.containers[username].remove();
|
||||
delete this.containers[username];
|
||||
}
|
||||
}
|
||||
81
src/NoteStorage.ts
Normal file
81
src/NoteStorage.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {LS_Prefix, LS_UserList} from './const';
|
||||
|
||||
export default class NoteStorage {
|
||||
|
||||
static inputData: {
|
||||
users: string[],
|
||||
notes: {
|
||||
user: string,
|
||||
note: string,
|
||||
resolved?: boolean,
|
||||
}[],
|
||||
settings: any,
|
||||
} = {
|
||||
users: [],
|
||||
notes: [],
|
||||
settings: {},
|
||||
};
|
||||
|
||||
static getSavedUserList(): string[] {
|
||||
return JSON.parse(localStorage.getItem(LS_UserList) || "[]");
|
||||
}
|
||||
|
||||
static getNote(username: string): string {
|
||||
return localStorage.getItem(LS_Prefix + username) || "";
|
||||
}
|
||||
|
||||
static addUserToSavedList(username: string) {
|
||||
let userList = this.getSavedUserList();
|
||||
|
||||
if (!userList.includes(username)) {
|
||||
userList.push(username);
|
||||
localStorage.setItem(LS_UserList, JSON.stringify(userList));
|
||||
}
|
||||
}
|
||||
|
||||
static removeUserFromSavedList(username: string) {
|
||||
let userList = this.getSavedUserList();
|
||||
userList = userList.filter(u => u !== username);
|
||||
if (userList.length) {
|
||||
localStorage.setItem(LS_UserList, JSON.stringify(userList));
|
||||
} else {
|
||||
localStorage.removeItem(LS_UserList);
|
||||
}
|
||||
}
|
||||
|
||||
static saveNote(username: string, note: string) {
|
||||
this.addUserToSavedList(username);
|
||||
localStorage.setItem(LS_Prefix + username, note);
|
||||
}
|
||||
|
||||
static deleteNote(username: string) {
|
||||
this.removeUserFromSavedList(username);
|
||||
localStorage.removeItem(LS_Prefix + username);
|
||||
}
|
||||
|
||||
static exportNotes() {
|
||||
const dataObject = {
|
||||
users: this.getSavedUserList(),
|
||||
notes: this.getSavedUserList().map(user => {
|
||||
return {user: user, note: this.getNote(user)};
|
||||
}),
|
||||
settings: {},
|
||||
};
|
||||
const data = JSON.stringify(dataObject)
|
||||
|
||||
const blob = new Blob([data], {type: 'text/json'});
|
||||
const elem = window.document.createElement('a');
|
||||
elem.href = window.URL.createObjectURL(blob);
|
||||
elem.download = 'twitch-notes-' + (Date.now()) + '.json';
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
document.body.removeChild(elem);
|
||||
}
|
||||
|
||||
static clearData() {
|
||||
const users = this.getSavedUserList();
|
||||
for(let user of users) {
|
||||
this.deleteNote(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,12 @@
|
||||
import {
|
||||
CustomButtonElement,
|
||||
CustomDivElement,
|
||||
CustomImgElement,
|
||||
CustomSpanElement
|
||||
} from "./CustomHTMLElement";
|
||||
import {ELEMENT_ButtonContainer} from "./const";
|
||||
import {openTwitchNote, toggleSettingsList} from "./HTMLTemplates";
|
||||
|
||||
export default class TwitchNote {
|
||||
|
||||
observer: MutationObserver | null = null;
|
||||
@@ -8,10 +17,12 @@ export default class TwitchNote {
|
||||
|
||||
init(chatContainerNode: Element) {
|
||||
this.chatContainerNode = chatContainerNode;
|
||||
console.log('CHAT CONTAINER INITIATED');
|
||||
this.startObserver(chatContainerNode);
|
||||
// add Notes management button
|
||||
this.addNotesButton();
|
||||
setTimeout(() => {
|
||||
this.addUserNoteBadges();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
startObserver(targetNode: Element) {
|
||||
@@ -42,8 +53,63 @@ export default class TwitchNote {
|
||||
}
|
||||
|
||||
addNotesButton() {
|
||||
const buttonContainer = document.querySelector(`.${ELEMENT_ButtonContainer}`);
|
||||
if (!buttonContainer) {
|
||||
setTimeout(() => this.addNotesButton(), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const container1 = new CustomDivElement();
|
||||
container1.setStyle({marginLeft: "0.5rem !important"});
|
||||
const container2 = new CustomDivElement();
|
||||
container1.setStyle({display: "inline-flex !important"});
|
||||
const settingsButton = new CustomButtonElement('twitch-notes-settings-button', 'Notes');
|
||||
|
||||
settingsButton.setClickListener(() => {
|
||||
toggleSettingsList();
|
||||
});
|
||||
|
||||
container2.appendCustomChild(settingsButton);
|
||||
container1.appendCustomChild(container2);
|
||||
|
||||
if (buttonContainer.lastChild) {
|
||||
buttonContainer.lastChild.insertBefore(
|
||||
container1.getElement(),
|
||||
buttonContainer.lastChild.lastChild
|
||||
);
|
||||
} else {
|
||||
buttonContainer.appendChild(container1.getElement());
|
||||
}
|
||||
}
|
||||
|
||||
addUserNoteBadges() {
|
||||
let nodes = document.getElementsByClassName("chat-line__message");
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes.item(i);
|
||||
if (!node) continue;
|
||||
const n = node.querySelector(".chat-author__display-name");
|
||||
if (!n) continue;
|
||||
const usernameContainer = node.querySelector(".chat-line__username-container");
|
||||
if (!usernameContainer) continue;
|
||||
const username = n.getAttribute('data-a-user');
|
||||
if (username) {
|
||||
let noteButton = usernameContainer.querySelector(".twitch-note");
|
||||
if (!noteButton) {
|
||||
const twitchNote = new CustomSpanElement('twitch-note');
|
||||
twitchNote.setStyle({cursor: 'pointer'});
|
||||
twitchNote.setClickListener(() => {
|
||||
openTwitchNote(username);
|
||||
});
|
||||
const img = new CustomImgElement();
|
||||
img.setStyle({height: '18px', paddingRight: '4px'});
|
||||
img.addAttribute('src', 'https://cdn.rdarius.lt/icons/32-id-card.png');
|
||||
twitchNote.appendCustomChild(img);
|
||||
usernameContainer.insertBefore(
|
||||
twitchNote.getElement(),
|
||||
usernameContainer.firstChild
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/const.ts
Normal file
3
src/const.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const LS_UserList = "twitch-note-all-users-list";
|
||||
export const LS_Prefix = "twitch-note-";
|
||||
export const ELEMENT_ButtonContainer = "chat-input__buttons-container";
|
||||
@@ -1,5 +1,6 @@
|
||||
import './styles/main.scss';
|
||||
import TwitchNote from "./TwitchNote";
|
||||
import Mouse from "./Mouse";
|
||||
|
||||
const ChatContainerClass = 'chat-scrollable-area__message-container';
|
||||
const twitchNote = new TwitchNote();
|
||||
@@ -24,6 +25,7 @@ function init() {
|
||||
if (!chatExists) {
|
||||
// chat container appeared
|
||||
twitchNote.init(targetNode);
|
||||
Mouse.setupListeners();
|
||||
}
|
||||
chatExists = true;
|
||||
} else {
|
||||
|
||||
10
src/types.ts
Normal file
10
src/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {CustomHTMLElement} from "./CustomHTMLElement";
|
||||
|
||||
export type CustomHTMLElementConstructor = {
|
||||
class?: string | string[],
|
||||
id?: string,
|
||||
css?: Partial<CSSStyleDeclaration>,
|
||||
attributes?: {[key: string]: string},
|
||||
body?: string | HTMLElement,
|
||||
customBody?: CustomHTMLElement,
|
||||
}
|
||||
Reference in New Issue
Block a user