AniCHAT - Discuss Anime Episodes

Get discussions from popular sites like MAL and Reddit for the anime you are watching right below your episode

Instalar o script?
Script sugerido do autor

Você também pode gostar de AniHIDE - Hide Unrelated Episodes.

Instalar o script
// ==UserScript==
// @name        AniCHAT - Discuss Anime Episodes
// @namespace   https://greasyfork.org/en/users/781076-jery-js
// @version     2.5.3
// @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://animesuge.*/anime/*
// @match       https://animesuge.to/anime/*
// @match       https://*.miruro.*/watch?id=*
// @match       https://*.miruro.tv/watch?id=*
// @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/gm-fetch@0.2.1
// ==/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: "animesuge",
		url: ["animesuge.to", "animesuge"],
        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 .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
	}
];

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)) ui.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}`);
}