diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.todo b/.todo deleted file mode 100644 index 6aefcd7..0000000 --- a/.todo +++ /dev/null @@ -1 +0,0 @@ -☐ Add button next to message \ No newline at end of file diff --git a/extension/background.js b/extension/background.js new file mode 100644 index 0000000..32d02cf --- /dev/null +++ b/extension/background.js @@ -0,0 +1,48 @@ +const activeTabs = {}; +let contentScriptRerunner = null; + +function runContentScript(tabId) { + chrome.tabs.executeScript(tabId, { + allFrames: true, + file: 'run.js', + }); +} + +function rerunContentScripts() { + // unfortunately we need to rerun periodically to handle iframes changing.. + // eventually fixed when we require the new host permission or chrome releases a content script api + for (const tabId of Object.keys(activeTabs)) { + runContentScript(parseInt(tabId, 10)); + } +} + +function registerDynamicContentScript() { + chrome.tabs.onUpdated.addListener((tabId, {status}, {url}) => { + if (!status || !url) { + return; + } + + runContentScript(tabId); + activeTabs[tabId] = true; + + if (contentScriptRerunner == null) { + contentScriptRerunner = setInterval(rerunContentScripts, 5000); + } + }); + chrome.tabs.onRemoved.addListener((tabId) => { + delete activeTabs[tabId]; + + if (contentScriptRerunner != null && Object.keys(activeTabs).length === 0) { + clearInterval(contentScriptRerunner); + contentScriptRerunner = null; + } + }); +} + +chrome.runtime.onInstalled.addListener(({reason}) => { + if (reason !== 'install') { + return; + } +}); + +registerDynamicContentScript(); diff --git a/extension/icon.png b/extension/icon.png new file mode 100644 index 0000000..04a1927 Binary files /dev/null and b/extension/icon.png differ diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..ecf172f --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,34 @@ +{ + "background": { + "scripts": ["background.js"] + }, + "browser_action": { + "default_popup": "settings.html" + }, + "content_scripts": [ + { + "all_frames": true, + "exclude_matches": [ + "*://*.twitch.tv/*.html", + "*://*.twitch.tv/*.html?*", + "*://*.twitch.tv/*.htm", + "*://*.twitch.tv/*.htm?*" + ], + "js": ["run.js"], + "matches": ["*://*.twitch.tv/*"] + } + ], + "description": "Add notes to twitch users", + "externally_connectable": { + "matches": ["*://*.twitch.tv/*"] + }, + "homepage_url": "https://rdarius.lt/projects/chrome-extensions/twitch-notes", + "icons": { + "128": "icon.png" + }, + "manifest_version": 2, + "name": "Twitch Notes", + "permissions": ["*://*.twitch.tv/*"], + "version": "0.0.1", + "web_accessible_resources": ["twitchnotes.js", "twitchnotes.css"] +} diff --git a/extension/notes-notepad-svgrepo-com.svg b/extension/notes-notepad-svgrepo-com.svg new file mode 100644 index 0000000..cdacaba --- /dev/null +++ b/extension/notes-notepad-svgrepo-com.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extension/run.js b/extension/run.js new file mode 100644 index 0000000..99be010 --- /dev/null +++ b/extension/run.js @@ -0,0 +1,110 @@ +(function twitchnotes() { + if (document.querySelector("script#twitchnotes")) { + return; + } + + const script = document.createElement("script"); + script.type = "text/javascript"; + script.src = chrome.runtime.getURL("twitchnotes.js"); + script.id = "twitchnotes"; + const head = document.getElementsByTagName("head")[0]; + if (!head) { + return; + } + + head.appendChild(script); + + const style = document.createElement("style"); + style.innerHTML = ` + :root { + --window-close: #f77669; + --window-minify: #ffcb6b; + --window-expand: #c3e88d; + + --background-header: #393d3e; + --background-code: #1f1e1e; + } + + .twitchnote img { + height: 18px; + padding: 0 4px 0; + } + + .twitch-note-container { + position: fixed; + z-index: 1000000; + min-width: 420px; + max-width: 95vw; + min-height: 240px; + max-height: 50vh; + background: var(--color-background-base); + border: var(--border-width-default) solid var(--color-border-base) !important; + } + + .twitch-note-header { + width: 100%; + height: 32px; + line-height: 32px; + border-bottom: var(--border-width-default) solid var(--color-border-base) !important; + position: relative; + cursor: move; + } + + .twitch-note-title { + color: var(--color-text-alt) !important; + font-size: var(--font-size-6) !important; + font-weight: var(--font-weight-semibold) !important; + text-transform: uppercase !important; + padding-left: calc((32px - var(--font-size-6)) / 2); + } + + .twitch-note-close-button { + position: absolute; + right: 5px; + top: 5px; + width: 22px; + height: 22px; + line-height: 22px; + text-align: center; + aspect-ration: 1; + color: var(--color-text-alt) !important; + font-size: var(--font-size-6) !important; + font-weight: var(--font-weight-semibold) !important; + text-transform: uppercase !important; + border: var(--border-width-default) solid var(--color-border-base) !important; + cursor: pointer; + } + + .twitch-note-content { + position: absolute; + top: 32px; + left: 0; + right: 0; + bottom: 32px; + background: transparent; + color: var(--color-text-base) !important; + font-family: var(--font-base); + vertical-align: baseline; + outline: none; + overflow: auto; + padding: calc((32px - var(--font-size-6)) / 2); + } + + .twitch-note-save-button { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 32px; + border-top: var(--border-width-default) solid var(--color-border-base) !important; + cursor: pointer; + color: var(--color-text-alt) !important; + font-size: var(--font-size-6) !important; + font-weight: var(--font-weight-semibold) !important; + text-transform: uppercase !important; + text-align: center; + line-height: 32px; + } +`; + head.appendChild(style); +})(); diff --git a/extension/settings.html b/extension/settings.html new file mode 100644 index 0000000..841d051 --- /dev/null +++ b/extension/settings.html @@ -0,0 +1,3 @@ +
+ Exporting, Importing and Data clearing will be available in the future +
diff --git a/extension/twitchnotes.js b/extension/twitchnotes.js new file mode 100644 index 0000000..8c731bc --- /dev/null +++ b/extension/twitchnotes.js @@ -0,0 +1,200 @@ +let mousePosition = { x: 0, y: 0 }; +let lastMousePosition = { x: 0, y: 0 }; +let openContainers = {}; +let isMouseDown = false; +let activeContainer = null; + +document.addEventListener("mousemove", handleMouseMove); +document.addEventListener("mouseup", handleMouseUp); + +function saveNote(username, note) { + let userList = JSON.parse( + localStorage.getItem("twitch-note-all-users-list") || "[]" + ); + + if (!userList.includes(username)) { + userList.push(username); + localStorage.setItem( + "twitch-note-all-users-list", + JSON.stringify(userList) + ); + } + + localStorage.setItem("twitch-note-" + username, note); +} +function getNote(username) { + return localStorage.getItem("twitch-note-" + username) || ""; +} + +function handleMouseMove(event) { + event = event || window.event; + if (isMouseDown) { + if (activeContainer) { + const dx = mousePosition.x - lastMousePosition.x; + const dy = mousePosition.y - lastMousePosition.y; + openContainers[activeContainer].style.top = + parseInt(openContainers[activeContainer].style.top) + dy + "px"; + openContainers[activeContainer].style.left = + parseInt(openContainers[activeContainer].style.left) + dx + "px"; + } + } + updateMousePosition(event.clientX, event.clientY); +} + +function handleMouseUp(event) { + activeContainer = null; + isMouseDown = false; +} + +function updateMousePosition(x, y) { + lastMousePosition = { ...mousePosition }; + mousePosition.x = x; + mousePosition.y = y; +} + +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) { + // CONTAINER + const container = document.createElement("div"); + container.style.left = mousePosition.x + "px"; + container.style.top = mousePosition.y + 10 + "px"; + container.classList.add("twitch-note-container"); + + // HEADER + const header = document.createElement("div"); + header.classList.add("twitch-note-header"); + header.addEventListener("mousedown", function close() { + activeContainer = username; + isMouseDown = true; + }); + const closeButton = document.createElement("span"); + closeButton.classList.add("twitch-note-close-button"); + closeButton.innerText = "X"; + closeButton.addEventListener("click", function close() { + removeContainer(username); + }); + header.appendChild(closeButton); + const title = document.createElement("span"); + title.classList.add("twitch-note-title"); + title.innerHTML = username; + header.appendChild(title); + container.appendChild(header); + + // BODY + const content = document.createElement("div"); + content.classList.add("twitch-note-content"); + content.setAttribute("contentEditable", true); + content.innerHTML = getNote(username); + + container.appendChild(content); + + const saveButton = document.createElement("div"); + saveButton.classList.add("twitch-note-save-button"); + saveButton.innerText = "SAVE"; + saveButton.addEventListener("click", function () { + saveNote(username, content.innerHTML); + removeContainer(username); + }); + + container.appendChild(saveButton); + + addContainer(username, container); +} + +(function runTwitchNotes() { + // Select the node that will be observed for mutations + + 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()); + + const settingsButton = document.createElement("div"); + settingsButton.innerHTML = "Notes"; + + buttonContainer.lastChild.insertBefore( + settingsButton, + buttonContainer.lastChild.firstChild + ); + + // 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, observer) => { + if (!changeInProgress) { + for (const mutation of mutationList) { + if (mutation.type === "childList") { + changeInProgress = true; + 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(".twitchnote"); + if (!noteButton) { + const twitchNote = document.createElement("span"); + twitchNote.classList.add("twitchnote"); + twitchNote.style.cursor = "pointer"; + twitchNote.addEventListener("click", () => { + openTwitchNote(username); + }); + + const img = document.createElement("img"); + img.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]++; + } + } + 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); +})();