Get discussions from popular sites like MAL and Reddit for the anime you are watching right below your episode
// ==UserScript== // @name AniCHAT - Discuss Anime Episodes // @namespace https://greasyfork.org/en/users/781076-jery-js // @version 2.5.7 // @description Get discussions from popular sites like MAL and Reddit for the anime you are watching right below your episode // @icon https://image.myanimelist.net/ui/OK6W_koKDTOqqqLDbIoPAiC8a86sHufn_jOI-JGtoCQ // @author Jery // @license MIT // @match https://yugenanime.*/* // @match https://yugenanime.tv/* // @match https://yugenanime.sx/* // @match https://animepahe.*/* // @match https://animepahe.com/*/ // @match https://anitaku.*/* // @match https://anitaku.bz/* // @match https://gogoanime.*/* // @match https://gogoanime.to/* // @match https://gogoanime3.*/* // @match https://gogoanime3.co/* // @match https://aniwave.*/watch/* // @match https://aniwave.to/watch/* // @match https://aniwave.vc/watch/* // @match https://aniwave.ti/watch/* // @match https://aniwatchtv.*/watch/* // @match https://aniwatchtv.to/watch/* // @match https://hianime.*/watch/* // @match https://hianime.to/watch/* // @match https://kayoanime.*/* // @match https://kayoanime.com/* // @match https://kaas.*/*/* // @match https://kaas.to/*/* // @match https://kickassanimes.*/*/* // @match https://kickassanimes.io/*/* // @match https://*.kickassanime.*/*/* // @match https://*.kickassanime.mx/*/* // @match https://anix.*/*/*/* // @match https://anix.to/*/*/* // @match https://anix.ac/*/*/* // @match https://anix.vc/*/*/* // @match https://animeflix.*/watch/* // @match https://animeflix.live/watch/* // @match https://animehub.*/watch/* // @match https://animehub.ac/watch/* // @match https://anim###ge.*/anime/* // @match https://anim###ge.to/anime/* // @match https://*.miruro.*/* // @match https://*.miruro.tv/watch?id=* // @match https://animez.org/*/epi-* // @grant GM_getValue // @grant GM_setValue // @grant GM_notification // @grant GM.xmlHttpRequest // @require https://unpkg.com/axios/dist/axios.min.js // Using GM_fetch for bypassing CORS // @require https://cdn.jsdelivr.net/npm/@trim21/[email protected] // ==/UserScript== /************************** * CONSTANTS ***************************/ // seconds to wait before loading the discussions (to avoid spamming the service) const TIMEOUT = 30000; // in milliseconds /*************************************************************** * ANIME SITES & SERVICES ***************************************************************/ const animeSites = [ { name: "yugenanime", url: ["yugenanime.tv", "yugenanime.sx"], chatArea: ".box.m-10-t.m-25-b.p-15", getAnimeTitle: () => document.querySelector(".ani-info-ep a > h1").textContent, getEpTitle: () => document.querySelector("h1.text-semi-bold.m-5-b").textContent, getEpNum: () => window.location.href.split("/")[6], styles: null, }, { name: "animepahe", url: ["animepahe.ru", "animepahe.com"], chatArea: ".theatre", getAnimeTitle: () => document.querySelector(".theatre-info > h1 > a").textContent.split(' - ')[0], getEpTitle: () => document.querySelector(".theatre-info > h1 > a").textContent.split(' - ')[0], getEpNum: () => document.querySelector(".dropup.episode-menu > button").innerText.split("Episode ")[1], styles: '.discussion-area { max-width:1100px; margin:15px auto 0; }', }, { name: "gogoanime", url: ['gogoanime3', 'gogoanimehd', 'gogoanime', 'anitaku'], chatArea: ".anime_video_body_comment_center", getAnimeTitle: () => document.querySelector(".anime-info > a").textContent, getEpTitle: () => document.querySelector(".anime-info > a").textContent, getEpNum: () => window.location.href.split("-episode-")[1], styles: `.chat-msg { color: white; font-size: 14px; } .discussion-title > a { font-size: 24px; color: goldenrod; }` }, { name: "aniwave", url: ['aniwave', 'lite.aniwave'], chatArea: "#comments", getAnimeTitle: () => document.querySelector(".name .title").textContent, getEpTitle: () => document.querySelector(".name .title").textContent, getEpNum: () => window.location.href.split("/ep-")[1], }, { name: "hianime", url: ["aniwatchtv", "hianime.to", "hianime.nz", "hianime.mm", "hianime.sx", "hianime"], chatArea: ".show-comments", getAnimeTitle: () => document.querySelector("h2.film-name > a").textContent, getEpTitle: () => document.querySelector("div.ssli-detail > .ep-name").textContent, getEpNum: () => waitForElm(".ssl-item.ep-item.active > .ssli-order").then(elm => elm.textContent), styles: `.chat-row .user-avatar { width: auto; overflow: visible; }` }, { name: "kayoanime", url: ["kayoanime.com"], chatArea: "#the-post", getAnimeTitle: () => document.querySelector("h1.entry-title").textContent.split(/Episode \d+ English.+/)[0].trim(), getEpTitle: () => document.querySelector(".toggle-head").textContent.trim(), getEpNum: () => document.querySelector("h1.entry-title").textContent.split(/Episode (\d+) English.+/)[1], }, { name: "kickassanime", url: ["kaas", "kickassanimes", "kickassanime"], chatArea: () => document.querySelector("#disqus_thread").parentElement, getAnimeTitle: () => document.querySelector(".text-h6").textContent, getEpTitle: () => document.querySelector(".text-h6").textContent, getEpNum: () => document.querySelector(".d-block .text-overline").textContent.split("Episode")[1].trim(), }, { name: "anix", url: ["anix"], chatArea: () => document.querySelector("#disqus_thread").parentElement, getAnimeTitle: () => document.querySelector(".ani-name").textContent, getEpTitle: () => document.querySelector(".ani-name").textContent, getEpNum: () => window.location.href.split("/ep-")[1], }, { name: "animeflix", url: ["animeflix"], chatArea: 'main', getAnimeTitle: () => document.querySelector(".details .title").textContent, getEpTitle: () => document.querySelector(".details .title").textContent, getEpNum: () => window.location.href.split("-episode-")[1], }, { name: "animehub", url: ["animehub"], chatArea: 'mawdawin', getAnimeTitle: () => document.querySelector(".dc-title").textContent, getEpTitle: () => document.querySelector(".dc-title").textContent, getEpNum: () => document.querySelector("#current_episode_name").textContent.split("Episode")[1].trim(), }, { name: "anim###ge", url: ["anim###ge.to", "anim###ge"], chatArea: '#comment', getAnimeTitle: () => document.querySelector("#media-info .maindata > h1").textContent, getEpTitle: () => document.querySelector("#media-info .maindata > h1").textContent, getEpNum: () => window.location.href.split("/ep-")[1], }, { name: "miruro", url: ["miruro.tv"], chatArea: () => document.querySelector("#disqus_thread").parentElement, getAnimeTitle: () => document.querySelector(".anime-title > a").textContent.trim(), getEpTitle: () => document.querySelector(".title-container .ep-title").textContent.trim(), getEpNum: () => document.querySelector(".title-container .ep-number").textContent.split(". ")[0], styles: `#AniCHAT a:-webkit-any-link { color: lightblue; } ul.discussion-list { padding-inline-start: 0px; }`, initDelay: 5000, // Time to wait (for page to load) before attaching the discussion area }, { name: "animez", url: ["animez.org"], chatArea: '#box_right_watch', getAnimeTitle: () => document.querySelector("#title-detail-manga").textContent, getEpTitle: () => document.querySelector("#title-detail-manga").textContent, getEpNum: () => document.querySelector(".wp-manga-chapter.active").textContent.replace("-Dub", "").trim(), } ]; const services = [ { name: "MyAnimeList", icon: "https://image.myanimelist.net/ui/OK6W_koKDTOqqqLDbIoPAiC8a86sHufn_jOI-JGtoCQ", url: "https://myanimelist.net/", _clientId: "dbe5cec5a2f33fdda148a6014384b984", async getDiscussion(animeTitle, epNum) { let animeId, topic, url, response, data; let headers = {headers: {"X-MAL-CLIENT-ID": this._clientId, 'x-requested-with': 'XMLHttpRequest', 'origin': window.location.origin}}; // get the anime's MAL id using MAL API (or use Jikan API if title is too long) try { if (animeTitle.length > 500) { url = `https://api.myanimelist.net/v2/anime?q=${animeTitle}&limit=1`; response = await GM_fetch(url, headers); data = await response.json(); animeId = data.data[0].node.id; } else { url = `https://api.jikan.moe/v4/anime?q=${animeTitle}&limit=1`; animeId = GM_getValue('cachedId_'+url, null); if (!animeId) { response = await GM_fetch(url, headers); data = await response.json(); animeId = data.data[0].mal_id; GM_setValue('cachedId_'+url, animeId); } } console.log(`animeId: ${animeId}`); } catch (e) { throw new Error(`Couldn't find the anime id. Retry after a while or switch to another service.\n${e.code} : ${e}`); } // get the discussion url from the anime try { url = `https://api.jikan.moe/v4/anime/${animeId}/forum`; response = await GM_fetch(url, headers); data = await response.json(); topic = data.data.find(it => it.title.includes(`Episode ${epNum} Discussion`)); console.log(`topic: ${topic}`); } catch (e) { throw new Error(`No discussion found. Retry after a while or switch to another service.\n${e.code} : ${e}`); } // get the forum page try { url = `https://api.myanimelist.net/v2/forum/topic/${topic.mal_id}?limit=100`; response = await GM_fetch(url, headers); data = await response.json(); console.log(`data: ${data}`); } catch (e) { throw new Error(`Error getting the discusssion (${topic}). Retry after a while or switch to another service.\n${e.code} : ${e}`); } let chats = []; data.data.posts.forEach((post) => { const user = post.created_by.name; const userLink = "https://myanimelist.net/profile/" + user; const avatar = post.created_by.forum_avator; const msg = this._parseBBCode(post.body); const timestamp = new Date(post.created_at).getTime(); const postId = data.data.posts.indexOf(post) + 1; const postLink = `https://myanimelist.net/forum/?goto=post&topicid=${topic.mal_id}&id=${post.id}`; chats.push(new Chat(user, userLink, avatar, msg, timestamp, null, postId, postLink)); }); const discussion = new Discussion(topic.title, topic.url, chats); return discussion; }, _parseBBCode(bbcode) { const mappings = [ { bbcode: /\[b\](.*?)\[\/b\]/g, html: "<strong>$1</strong>" }, { bbcode: /\[i\](.*?)\[\/i\]/g, html: "<em>$1</em>" }, { bbcode: /\[u\](.*?)\[\/u\]/g, html: "<u>$1</u>" }, { bbcode: /\[s\](.*?)\[\/s\]/g, html: "<s>$1</s>" }, { bbcode: /\[url=(.*?)\](.*?)\[\/url\]/g, html: '<a href="$1">$2</a>' }, { bbcode: /\[img.*?\](.*?)\[\/img\]/g, html: '<img src="$1" alt="">' }, { bbcode: /\[code\]([\s\S]*?)\[\/code\]/g, html: "<code>$1</code>" }, { bbcode: /\[quote\]/g, html: '<blockquote class="quote" style="font-size: 90%; border: 1px solid; padding: 5px;">' }, { bbcode: /\[quote=(.*?)\s*(message=\d+)?\]/g, html: '<blockquote class="quote" style="font-size: 90%; border: 1px solid; padding: 5px;"><h4>$1 Said:</h4>' }, { bbcode: /\[\/quote\]/g, html: '</blockquote>' }, { bbcode: /\[color=(.*?)\](.*?)\[\/color\]/g, html: '<span style="color: $1;">$2</span>' }, { bbcode: /\[size=(.*?)\](.*?)\[\/size\]/g, html: '<span style="font-size: $1;">$2</span>' }, { bbcode: /\[center\](.*?)\[\/center\]/g, html: '<div style="text-align: center;">$1</div>' }, { bbcode: /\[list\](.*?)\[\/list\]/g, html: "<ul>$1</ul>" }, { bbcode: /\[list=(.*?)\](.*?)\[\/list\]/g, html: '<ol start="$1">$2</ol>' }, { bbcode: /\[\*\](.*?)\[\/\*\]/g, html: "<li>$1</li>" }, { bbcode: /\[spoiler\]([\s\S]*?)\[\/spoiler\]/g, html: '<div class="spoiler"><input type="button" onclick="this.nextSibling.style.display=\'inline-block\';this.style.display=\'none\';" value="Show spoiler" style="display: inline-block;"><span class="spoiler_content" style="display: none;"><input type="button" onclick="this.parentNode.style.display=\'none\';this.parentNode.parentNode.childNodes[0].style.display=\'inline-block\';" value="Hide spoiler">$1</span></div>' }, { bbcode: /\[spoiler=(.*?)\]([\s\S]*?)\[\/spoiler\]/g, html: '<div class="spoiler"><input type="button" onclick="this.nextSibling.style.display=\'inline-block\';this.style.display=\'none\';" value="Show $1" style="display: inline-block;"><span class="spoiler_content" style="display: none;"><input type="button" onclick="this.parentNode.style.display=\'none\';this.parentNode.parentNode.childNodes[0].style.display=\'inline-block\';" value="Hide $1">$2</span></div>' }, { bbcode: /\[yt\](.*?)\[\/yt\]/g, html: '<iframe width="560" height="315" src="https://www.youtube.com/embed/$1" frameborder="0" allowfullscreen></iframe>' }, { bbcode: /\[yt\](.*?)\?(start|end)=(\d+)\[\/yt\]/g, html: '<iframe width="560" height="315" src="https://www.youtube.com/embed/$1?$2=$3" frameborder="0" allowfullscreen></iframe>' }, { bbcode: /\[yt\](.*?)\?start=(\d+)&end=(\d+)\[\/yt\]/g, html: '<iframe width="560" height="315" src="https://www.youtube.com/embed/$1?start=$2&end=$3" frameborder="0" allowfullscreen></iframe>' }, { bbcode: /@(\S+)/g, html: '<a href="https://myanimelist.net/profile/$1" target="_blank">@$1</a>' }, ]; let html = bbcode; for (const mapping of mappings) { html = html.replace(mapping.bbcode, mapping.html); } return html; } }, { name: "Reddit", icon: "https://www.redditstatic.com/desktop2x/img/favicon/apple-icon-57x57.png", url: "https://www.reddit.com/", _clientId: "dbe5cec5a2f33fdda148a6014384b984", async getDiscussion(animeTitle, epNum) { let animeId, topic, url, response, posts; let headers = {headers: {'x-requested-with': 'XMLHttpRequest', 'origin': window.location.origin}}; // get the anime's MAL id try { url = `https://api.jikan.moe/v4/anime?q=${animeTitle}&limit=1`; animeId = GM_getValue('cachedId_'+url, ''); if (animeId == '') { response = await GM_fetch(url, headers); data = await response.json(); if (data.data.length > 0) { animeId = data.data[0].mal_id; GM_setValue('cachedId_'+url, animeId); } } } catch (e) { throw new Error(`Couldn't find the anime id. Retry after a while or switch to another service.\n${e.code} : ${e.message}`); } // Get the discussion try { url = `https://api.reddit.com/r/anime/search.json?q=${animeTitle}+-+Episode+${epNum}+discussion+author:AutoLovepon&restrict_sr=on&include_over_18=on&sort=relevance&limit=50`; response = await axios.get(url); topic = response.data.data.children.find(it => it.data.title.includes(` - Episode ${epNum} discussion`) && it.data.selftext.includes(`[MyAnimeList](https://myanimelist.net/anime/${animeId}`))?.data; } catch (e) { throw new Error(`No discussion found. Retry after a while or switch to another service. (You are probably being rate limited)\n${e.code} : ${e.message}`); } // get the comments in the discussion try { url = topic.url.replace('www.reddit.com', 'api.reddit.com'); response = await axios.get(url); posts = response.data[1].data.children; if (posts[0].data.author == "AutoModerator") posts.shift(); // skip the first bot post } catch (e) { throw new Error(`Error getting the discusssion. Retry after a while or switch to another service.\n${e.code} : ${e.message}`); } let chats = []; for (let post of posts) chats.push(this._processPost(post.data)); const discussion = new Discussion(topic.title, topic.url, chats); return discussion; }, _processPost(post) { const user = post.author; const userLink = "https://www.reddit.com/user/" + user; const avatar = axios.get(`https://api.reddit.com/user/${user}/about`).then(r=>r.data.data.icon_img.split('?')[0]); const msg = ((el) => { el.innerHTML = post.body_html; return el.value; })(document.createElement('textarea')); const timestamp = post.created_utc * 1000; let replies = []; if (post.replies && post.replies.data) for (let reply of post.replies.data.children) if(reply.data?.body_html) replies.push(this._processPost(reply.data)); return new Chat(user, userLink, avatar, msg, timestamp, replies, post.id, "https://www.reddit.com"+post.permalink); } }, ]; /*************************************************************** * Classes for handling various data like settings & discussions ***************************************************************/ // User settings class UserSettings { constructor(usernames = {}) { this.usernames = usernames; } static load() { return GM_getValue("userSettings", new UserSettings()); } } // Class to hold each row of a discussion class Chat { constructor(user, userLink, avatar, msg, timestamp, replies, id, link) { this.user = user; this.userLink = userLink; this.avatar = avatar; this.msg = msg; this.timestamp = timestamp; this.replies = replies; this.id = id; this.link = link; } getRelativeTime() { const now = new Date().getTime(); const diff = now - this.timestamp; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); const weeks = Math.floor(days / 7); const months = Math.floor(days / 30); const years = Math.floor(days / 365); if (years > 0) { return `${years} ${years === 1 ? "year" : "years"} ago`; } else if (months > 0) { return `${months} ${months === 1 ? "month" : "months"} ago`; } else if (weeks > 0) { return `${weeks} ${weeks === 1 ? "week" : "weeks"} ago`; } else if (days > 0) { return `${days} ${days === 1 ? "day" : "days"} ago`; } else if (hours > 0) { return `${hours} ${hours === 1 ? "hour" : "hours"} ago`; } else if (minutes > 0) { return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`; } else { return `${seconds} ${seconds === 1 ? "second" : "seconds"} ago`; } } } // Class to hold the complete discussions class Discussion { constructor(title, link, chats) { this.title = title; this.link = link; this.chats = chats; } } /*************************************************************** * The UI elements ***************************************************************/ // generate the discussion area async function generateDiscussionArea() { document.querySelector("#AniCHAT")?.remove(); // Remove existing discussion area (if it exists) const discussionArea = document.createElement("div"); discussionArea.id = "AniCHAT"; discussionArea.className = "discussion-area"; const discussionTitle = document.createElement("h3"); discussionTitle.className = "discussion-title"; const discussionTitleText = document.createElement("a"); discussionTitleText.textContent = `${await site.getAnimeTitle()} Episode ${await site.getEpNum()} Discussion`; discussionTitleText.title = "Click to view the original discussion"; discussionTitleText.target = "_blank"; discussionTitle.appendChild(discussionTitleText); const serviceSwitcher = buildServiceSwitcher(); discussionTitle.appendChild(serviceSwitcher); const discussionList = document.createElement("ul"); discussionList.className = "discussion-list"; discussionArea.appendChild(discussionTitle); discussionArea.appendChild(discussionList); return discussionArea; } function buildServiceSwitcher() { const servicesArea = document.createElement('div'); servicesArea.id = 'service-switcher'; servicesArea.innerHTML = `<img class="service-icon selected" title="Powered by ${service.name}" src="${service.icon}"><a style="padding-right:5px">▶</a>`; services.forEach(it => { servicesArea.innerHTML += `<img class="service-icon other" data-opt="${services.indexOf(it)}" title="Switch to ${it.name}" src="${it.icon}" style="cursor:pointer;">`; }); servicesArea.querySelectorAll('.other').forEach(it => { it.addEventListener('click', () =>{ const serviceOpt = parseInt(it.getAttribute('data-opt')); console.log(serviceOpt); GM_setValue("service", serviceOpt); service = services[serviceOpt]; run(); }); }); return servicesArea; } // build a row for a single chat in the discussion async function buildChatRow(chat) { const chatRow = document.createElement("li"); chatRow.className = "chat-row"; const chatContent = document.createElement("div"); chatContent.className = "chat-content"; const userAvatar = document.createElement("div"); userAvatar.className = "user-avatar"; userAvatar.innerHTML = `<img src="${service.icon}" alt="${chat.user}">`; if (chat.avatar instanceof Promise) chat.avatar.then(avatarUrl => userAvatar.firstChild.src = avatarUrl); else userAvatar.firstChild.src = chat.avatar; const userMsg = document.createElement("div"); userMsg.className = "user-msg"; const name = document.createElement("a"); name.className = "chat-name"; name.textContent = chat.user; name.href = chat.userLink; name.target = "_blank"; const time = document.createElement("span"); time.className = "chat-time"; time.textContent = chat.getRelativeTime(); time.title = new Date(chat.timestamp).toLocaleString(undefined, { weekday: "long", year: "numeric", month: "long", day: "numeric", hour: "numeric", minute: "numeric", hour12: true, }); const msg = document.createElement("span"); msg.className = "chat-msg"; msg.innerHTML = chat.msg; const chatId = document.createElement("a"); chatId.className = "chat-id"; chatId.textContent = `#${chat.id}`; chatId.href = chat.link; chatId.target = "_blank"; userMsg.appendChild(chatId); userMsg.appendChild(name); userMsg.appendChild(time); userMsg.appendChild(msg); chatContent.appendChild(userAvatar); chatContent.appendChild(userMsg); chatRow.appendChild(chatContent); if (chat.replies && chat.replies.length > 0) { const repliesDiv = document.createElement("div"); repliesDiv.className = "reply"; for (let reply of chat.replies) { const replyRow = await buildChatRow(reply); repliesDiv.appendChild(replyRow); } chatRow.appendChild(repliesDiv); } return chatRow; } // Show countdown for loading the discussion. function showLoading(timeout = TIMEOUT, onComplete) { const loadingArea = document.createElement("div"); loadingArea.className = "loading-anichat"; // Loading UI elements const loadingElement = document.createElement("div"); loadingElement.innerHTML = `<img src="https://flyclipart.com/thumb2/explosion-gif-transparent-transparent-gif-sticker-741584.png" style="width: 150px; margin-right: 10px;">`; loadingElement.style.cssText = `display: flex; align-items: center;`; const progressBar = document.createElement("div"); progressBar.className = "progress-bar"; progressBar.style.cssText = `width: 100%; height: 10px; background-color: #ccc; position: relative; margin-bottom: 10px;`; const progressFill = document.createElement("div"); progressFill.className = "progress-fill"; progressFill.style.cssText = `width: 0%; height: 100%; background-color: #4CAF50; position: absolute; top: 0; left: 0; transition: width 0.1s linear;`; const message = document.createElement("span"); message.textContent = `This ${timeout / 1000} secs timeout is set to reduce the load on the service`; message.style.cssText = "font-size: 14px; color: darkgrey;"; const skipButton = document.createElement("button"); skipButton.textContent = "Skip Waiting"; skipButton.style.cssText = `background: #4CAF50; color: white; border: none; padding: 5px 15px; border-radius: 5px; cursor: pointer; font-weight: bold; transition: transform 0.2s ease; margin-top: 10px; align-self: start;`; skipButton.onmouseover = () => skipButton.style.transform = 'scale(1.1)'; skipButton.onmouseout = () => skipButton.style.transform = 'scale(1)'; const colDiv = document.createElement("div"); colDiv.style.cssText = "display: flex; flex-direction: column; align-items: center;"; colDiv.appendChild(message); colDiv.appendChild(skipButton); // Assemble UI progressBar.appendChild(progressFill); loadingElement.appendChild(colDiv); loadingArea.appendChild(loadingElement); loadingArea.appendChild(progressBar); // Loading logic let countdown = timeout; let skipRequested = false; const countdownInterval = setInterval(() => { if (!skipRequested) { countdown -= 100; progressFill.style.width = `${100 - (countdown / timeout) * 100}%`; if (countdown <= 0) complete(); } }, 100); function complete() { clearInterval(countdownInterval); message.textContent = "Hold on tight~ The discussions are being loaded..." onComplete(); } skipButton.onclick = () => { skipRequested = true; skipButton.remove(); progressFill.style.width = '100%'; complete(); }; if (!(document.body.isFirstLoad??true)) skipButton.click(); // Skip the loading timeout if not first load return loadingArea; } // Add CSS styles to the page const styles = ` .discussion-area { border-radius: 10px; padding: 10px; } .discussion-title { display: flex; justify-content: space-between; margin-bottom: 20px; } .discussion-title > a { margin-right: 20px; } .service-icon { height: 25px; padding-right: 10px; } #service-switcher { width: 7%; transition: width 0.3s ease-in-out; overflow: hidden; display: flex; } #service-switcher:hover { width: ${8+5*services.length}%; } ul.discussion-list { overflow: auto; max-height: 90vh; } .chat-row { display: flex; flex-direction: column; padding: 10px 0; border-top: 1px solid #eee; } .chat-content { display: flex; flex-direction: row; } .chat-row > .reply { display: flex; flex-direction: column; padding-left: 55px; border-left: 0.7px solid #eee; } .user-avatar { width: 55px; height: 55px; margin-right: 10px; } .user-avatar > img { width: 55px; height: 55px; object-fit: cover; border-radius: 15px; } .user-msg { display: flex; width: 100%; flex-direction: column; } .chat-id { margin-bottom: -20px; font-size: 16px; align-self: end; color: grey !important; opacity: 0.3; transition: opacity 0.2s; } .chat-id:hover { opacity: 1; } .chat-name { font-weight: bold; font-size: 15px; align-self: start; } .chat-time { font-size: 12px; font-weight: bold; padding-top: 5px; color: darkgrey; } .chat-msg { padding: 10px 0; } .chat-msg img { max-width: 100%; } .error-message { color: red; white-space: pre-wrap; } `; /*************************************************************** * Initialize all data and setup menu commands ***************************************************************/ // User settings let userSettings = UserSettings.load(); // Site instance let site = getCurrentSite(); // Service instance let service = services[GM_getValue("service", 0)]; /*************************************************************** * Functions for working of the script ***************************************************************/ // Returns a promise of the given element. Resolves when the element is found in the DOM. function waitForElm(selector) { return new Promise(resolve => { if (document.querySelector(selector)) { let elm = document.querySelector(selector); // console.log(`Element Found!!: ${elm.textContent}`); return resolve(elm); } const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { let elm = document.querySelector(selector); // console.log(`Element Detected!: ${elm.textContent}`); resolve(elm); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } // Get the current website based on the URL function getCurrentSite() { const currentUrl = window.location.href.toLowerCase(); return animeSites.find((website) => website.url.some((site) => currentUrl.includes(site))); } // Use IntersectionObserver to call the callback when the element is in view function withIntersectionObserver(element, callback) { new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { callback(); observer.disconnect(); } }); }, { threshold: 0.1 }).observe(element); if(!callback) return new Promise(r => callback=r); } // Run the script async function run() { console.info(`Running AniCHAT on ${site.name}...`); const discussionArea = await generateDiscussionArea(); // Add to page using fallback selectors const selectors = [ { selector: () => site.chatArea && typeof site.chatArea === "string" ? document.querySelector(site.chatArea) : site.chatArea(), prepend: false }, { selector: () => document.querySelector('#main > .container'), prepend: false }, { selector: () => document.querySelector('#footer'), prepend: true }, { selector: () => document.querySelector('footer'), prepend: true }, { selector: () => document.body, prepend: false }, ]; for (let {selector, prepend} of selectors) { try { const element = selector(); prepend ? element.prepend(discussionArea) : element.appendChild(discussionArea); break; } catch (error) { continue; } } // Add styles const styleElement = document.createElement("style"); styleElement.textContent = styles + (site.styles || ''); discussionArea.append(styleElement); // Loading and discussion loading logic const loadDiscussion = async () => { document.body.isFirstLoad = false; // A flag to disable loading timeout on subsequent loads try { const discussion = await service.getDiscussion(await site.getAnimeTitle(), await site.getEpNum()); discussion.chats.forEach(async chat => { discussionArea.querySelector("ul").appendChild(await buildChatRow(chat)); }); discussionArea.querySelector(".discussion-title a").href = discussion.link; discussionArea.querySelector(".discussion-title a").textContent = discussion.title; } catch (error) { console.error(error); const errorElement = document.createElement("span"); errorElement.className = "error-message"; errorElement.textContent = `AniCHAT:\n${error.stack}\n\nCheck the console logs for more detail.`; discussionArea.appendChild(errorElement); } finally { document.querySelector(".loading-anichat")?.remove(); } }; // Initial loading with timeout discussionArea.appendChild(showLoading(TIMEOUT, () => { withIntersectionObserver(discussionArea, loadDiscussion); })); } // Workaround for SPA sites like Miruro for which the script doesn't auto reload on navigation function initScript() { const initDelay = site.initDelay || 0; setTimeout(run, initDelay); // Handle SPA navigation let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; console.log('URL changed, re-running AniCHAT'); setTimeout(run, initDelay); } }).observe(document.querySelector('body'), { subtree: true, childList: true }); } try { initScript(); } catch (e) { console.error(`${e.message}\n\n${e.stack}`); }