FB全部留言小幫手

Easy way to show all comments.

目前為 2024-11-30 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Facebook All Comments Helper
// @name:zh-TW   FB全部留言小幫手
// @name:zh-CN   FB全部留言小帮手
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Easy way to show all comments.
// @description:zh-tw  讓您更快打開全部留言
// @description:zh-cn  让您更快打开全部留言
// @author       Xuitty
// @match        https://www.facebook.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=facebook.com
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license      MIT
// ==/UserScript==

/**
 * string array for detecting the menu button
 */

const langs = {
	de: ["Relevanteste", "Top-Kommentare", "Am zutreffendsten", "Neueste zuerst", "Neueste", "Alle Kommentare"],
	en: ["Top comments", "Most relevant", "Most applicable", "Most recent", "Newest", "All comments"],
	es: ["Comentarios destacados", "Más relevantes", "Más pertinentes", "Más recientes", "Más recientes", "Todos los comentarios"],
	hu: ["A legfontosabb hozzászólások", "A legrelevánsabbak", "A témához leginkább illők", "A legújabbak", "A legutóbbiak", "Az összes hozzászólás"],
	ja: ["トップコメント", "関連度の高い順", "最も適切", "新しい順", "新しい順", "すべてのコメント"],
	ko: ["관련성 높은 댓글", "참여도 높은 댓글", "적합성 높은 순", "최신순", "날짜 내림차순", "모든 댓글"],
	fr: ["Plus pertinents", "Les meilleurs commentaires", "Les plus pertinents", "Plus récents", "Les plus récents", "Tous les commentaires"],
	sk: ["Top komentáre", "Najrelevantnejšie", "Najvhodnejšie", "Najnovšie", "Najnovšie", "Všetky komentáre"],
	sl: ["Najbolj priljubljeni komentarji", "Najustreznejši", "Najustreznejše", "Najnovejši", "Najnovejši", "Vsi komentarji"],
	"zh-Hans": ["热门评论", "最相关", "最合适", "从新到旧", "最新", "所有评论"],
	"zh-Hant": ["最熱門留言", "最相關", "最相關", "最新", "由新到舊", "所有留言"],
};

/**
 * string array for notification
 */

const notificationStr = {
	de: ["Wechseln zu allen Kommentaren!", "Wechseln zu den neuesten Kommentaren!"],
	en: ["Switch to All Comments!", "Switch to Latest Comments!"],
	es: ["Cambiar a todos los comentarios!", "Cambiar a los comentarios más recientes!"],
	hu: ["Váltás az összes hozzászólásra!", "Váltás a legújabb hozzászólásokra!"],
	ja: ["すべてのコメントに切り替え!", "最新のコメントに切り替え!"],
	ko: ["모든 댓글로 전환!", "최신 댓글로 전환!"],
	fr: ["Passer à tous les commentaires!", "Passer aux commentaires les plus récents!"],
	sk: ["Prepnúť na všetky komentáre!", "Prepnúť na najnovšie komentáre!"],
	sl: ["Preklopite na vse komentarje!", "Preklopite na najnovejše komentarje!"],
	"zh-Hant": ["切換到所有留言!", "切換到最新留言!"],
	"zh-Hans": ["切换到所有评论!", "切换到最新评论!"],
};

/**
 * string array for settings
 */

const settingsStr = {
	"zh-Hant": {
		settings: "設定",
		autoDetect: "自動切換為全部/最新留言",
		notifyEnabled: "操作通知",
		isAll: "全部留言/最新留言",
		isScroll: "是否開啟捲動",
		scrollBehavior: "捲動特效",
		hideSettings: "隱藏選單",
		smooth: "平滑",
		auto: "無",
		openMenu: "開啟選單",
		needRefresh: "需要重新整理頁面應用自動切換",
	},
	"zh-Hans": {
		settings: "设置",
		autoDetect: "自动切换为全部/最新评论",
		notifyEnabled: "操作通知",
		isAll: "全部评论/最新评论",
		isScroll: "是否开启滚动",
		scrollBehavior: "滚动特效",
		hideSettings: "隐藏菜单",
		smooth: "平滑",
		auto: "无",
		openMenu: "打开菜单",
		needRefresh: "需要重新刷新页面应用自动切换",
	},
	en: {
		settings: "Settings",
		autoDetect: "Auto detect all/latest comments",
		notifyEnabled: "Notify after action",
		isAll: "All comments/Latest comments",
		isScroll: "Enable scroll effect",
		scrollBehavior: "Scroll behavior",
		hideSettings: "Hide settings",
		smooth: "Smooth",
		auto: "None",
		openMenu: "Open menu",
		needRefresh: "Need to refresh the page to apply auto detection",
	},
	de: {
		settings: "Einstellungen",
		autoDetect: "Automatisch alle/neuesten Kommentare erkennen",
		notifyEnabled: "Nach Aktion benachrichtigen",
		isAll: "Alle Kommentare/Neueste Kommentare",
		isScroll: "Bildlauf aktivieren",
		scrollBehavior: "Bildlaufverhalten",
		hideSettings: "Einstellungen ausblenden",
		smooth: "Sanft",
		auto: "Keine",
		openMenu: "Menü öffnen",
		needRefresh: "Die Seite muss aktualisiert werden, um die automatische Erkennung anzuwenden",
	},
	es: {
		settings: "Ajustes",
		autoDetect: "Detectar automáticamente todos/los comentarios más recientes",
		notifyEnabled: "Notificar después de la acción",
		isAll: "Todos los comentarios/Comentarios más recientes",
		isScroll: "Activar efecto de desplazamiento",
		scrollBehavior: "Comportamiento de desplazamiento",
		hideSettings: "Ocultar ajustes",
		smooth: "Suave",
		auto: "Ninguno",
		openMenu: "Abrir menú",
		needRefresh: "Necesita actualizar la página para aplicar la detección automática",
	},
	fr: {
		settings: "Paramètres",
		autoDetect: "Détecter automatiquement tous/les derniers commentaires",
		notifyEnabled: "Notifier après l'action",
		isAll: "Tous les commentaires/Derniers commentaires",
		isScroll: "Activer l'effet de défilement",
		scrollBehavior: "Comportement de défilement",
		hideSettings: "Masquer les paramètres",
		smooth: "Doux",
		auto: "Aucun",
		openMenu: "Ouvrir le menu",
		needRefresh: "Besoin de rafraîchir la page pour appliquer la détection automatique",
	},
	hu: {
		settings: "Beállítások",
		autoDetect: "Az összes/legújabb hozzászólás automatikus észlelése",
		notifyEnabled: "Értesítés az akció után",
		isAll: "Minden megjegyzés/Legfrissebb megjegyzések",
		isScroll: "Gördítési hatás engedélyezése",
		scrollBehavior: "Gördülési viselkedés",
		hideSettings: "Beállítások elrejtése",
		smooth: "Simít",
		auto: "Nincs",
		openMenu: "Menü megnyitása",
		needRefresh: "Az automatikus észlelés alkalmazásához frissíteni kell az oldalt",
	},
	ja: {
		settings: "設定",
		autoDetect: "すべて/最新のコメントを自動検出",
		notifyEnabled: "アクション後に通知",
		isAll: "すべてのコメント/最新のコメント",
		isScroll: "スクロール効果を有効にする",
		scrollBehavior: "スクロール動作",
		hideSettings: "設定を非表示",
		smooth: "スムーズ",
		auto: "なし",
		openMenu: "メニューを開く",
		needRefresh: "自動検出を適用するにはページを更新する必要があります",
	},
	ko: {
		settings: "설정",
		autoDetect: "모든/최신 댓글 자동 감지",
		notifyEnabled: "작업 후 알림",
		isAll: "모든 댓글/최신 댓글",
		isScroll: "스크롤 효과 사용",
		scrollBehavior: "스크롤 동작",
		hideSettings: "설정 숨기기",
		smooth: "부드러운",
		auto: "없음",
		openMenu: "메뉴 열기",
		needRefresh: "자동 감지를 적용하려면 페이지를 새로 고쳐야 합니다",
	},
	sk: {
		settings: "Nastavenia",
		autoDetect: "Automaticky zistiť všetky/najnovšie komentáre",
		notifyEnabled: "Upozorniť po akcii",
		isAll: "Všetky komentáre/Najnovšie komentáre",
		isScroll: "Povoliť posuvný efekt",
		scrollBehavior: "Správanie posuvu",
		hideSettings: "Skryť nastavenia",
		smooth: "Hladký",
		auto: "Žiadny",
		openMenu: "Otvoriť menu",
		needRefresh: "Na použitie automatického zistenia je potrebné obnoviť stránku",
	},
	sl: {
		settings: "Nastavitve",
		autoDetect: "Samodejno zaznani vsi/najnovejši komentarji",
		notifyEnabled: "Obvesti po dejanju",
		isAll: "Vsi komentarji/Najnovejši komentarji",
		isScroll: "Omogoči učinek drsenja",
		scrollBehavior: "Vedenje drsenja",
		hideSettings: "Skrij nastavitve",
		smooth: "Gladko",
		auto: "Brez",
		openMenu: "Odpri meni",
		needRefresh: "Za uporabo samodejnega zaznavanja je treba osvežiti stran",
	},
};


/**
 * get the language of the fb
 * @returns the language of the fb
 */
function detectLang() {
	return document.getElementById("facebook")?.getAttribute("lang") || "en";
}

/**
 * get the settings string array
 * @returns the settings string array
 */

function getSettingsStr() {
	return settingsStr[detectLang()] || settingsStr.en;
}

/**
 * get the xpath for menu
 * @returns the xpath for menu
 */

function getMenuButtonXPath() {
	const lang = langs[detectLang()] || langs.en;
	return `//span[not(@style) and (text()='${lang[0]}' or text()='${lang[1]}' or text()='${lang[2]}' or text()='${lang[3]}' or text()='${lang[4]}' or text()='${lang[5]}')]`;
}

/**
 * handle the click the comment button or the right bottom comment count
 */

function handleClickOutside() {
	if (settings.isAll) showAllComment();
	else showLatestComment();
}

/**
 * wait for the element to appear in the DOM
 * @param {string} xpath 
 * @param {Function} callback 
 * @param {number} timeout 
 * @param {number} interval
 */

function waitForElement(xpath, callback, timeout = 3000, intervalTime = 100) {
	const startTime = Date.now();
	const interval = setInterval(() => {
		const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
		if (element) {
			clearInterval(interval);
			callback(element);
		} else if (Date.now() - startTime > timeout) {
			clearInterval(interval);
			console.warn("Timeout: Element not found for XPath", xpath);
		}
	}, intervalTime);
}

/**
 * 
 * @param {Element} element 
 */

function safeClick(element) {
	try {
		element.click();
	} catch (err) {
		console.error("Error clicking element:", err);
	}
}

/**
 * show the notification to user after the action
 * override is for mode in settings is allcomments and user press ctrl+dblclick/ctrl+insert
 * @param {boolean} reverse 
 */

async function notifyUser(reverse = false) {
	const notification = document.createElement("div");
	notification.setAttribute("id", "FBAllCommentsHelperNotification");
	notification.style.position = "fixed";
	notification.style.bottom = "20px";
	notification.style.left = "20px";
	notification.style.backgroundColor = "rgba(0,0,0,1)";
	notification.style.color = "white";
	notification.style.padding = "10px";
	notification.style.borderRadius = "5px";
	notification.style.zIndex = "9999";
	const notifyingTimeout = (Number) (await GM.getValue("notifyingTimeout"));
	if (reverse) {
		notification.textContent = notificationStr[detectLang()][settings.isAll ? 1 : 0] || notificationStr.en[settings.isAll ? 1 : 0];
	} else {
		notification.textContent = notificationStr[detectLang()][settings.isAll ? 0 : 1] || notificationStr.en[settings.isAll ? 0 : 1];
	}
	document.body.appendChild(notification);
	if (notifyingTimeout) {
		document.getElementById("FBAllCommentsHelperNotification").remove();
		clearTimeout(notifyingTimeout);
	};
	const id = setTimeout(async () => {
		notification.remove();
		await GM.deleteValue("notifyingTimeout");
	}, 3000);
	await GM.setValue("notifyingTimeout", id);
}

/**
 * parse the user action
 * @param {*} e 
 * @returns 
 */

function actionParser(e) {
	if (e.type === "dblclick" && e.ctrlKey) {
		!settings.isAll ? showAllComment(true) : showLatestComment(true);
		return;
	}
	if (e.type === "dblclick") {
		settings.isAll ? showAllComment() : showLatestComment();
		return;
	}
	if (e.code === "Insert" && e.ctrlKey) {
		!settings.isAll ? showAllComment(true) : showLatestComment(true);
		return;
	}
	if (e.code === "Insert") {
		settings.isAll ? showAllComment() : showLatestComment();
		return;
	}
}

/**
 * detecting the changes in the DOM
 * @param {Function} callback 
 */

function observeDOM(callback) {
	const observer = new MutationObserver((mutations) => {
		mutations.forEach(() => callback());
	});
	observer.observe(document.body, { childList: true, subtree: true });
}

/**
 * show all comments
 */

function showAllComment(reverse = false) {
	waitForElement(  //getting the menu
		getMenuButtonXPath(),
		(element) => {
			if (settings.isScroll) {
				element.scrollIntoView({ behavior: settings.scrollBehavior, block: "center" });
			}
			setTimeout(() => {
				safeClick(element);
			}, 100);
			waitForElement(  //getting the items in menu
				"//*[@role='menuitem']",
				(element) => {
					const menuItems = document.querySelectorAll('*[role="menuitem"]');
					if (menuItems.length > 1) {
						safeClick(menuItems[menuItems.length - 1]);
						if (settings.notifyEnabled) {
							notifyUser(reverse);
						}
					}
				}
			);
		}
	);
}

/**
 * show latest comment
 * override is for mode in settings is allcomments and user press ctrl+dblclick/ctrl+insert
 * @param {boolean} reverse 
 */
function showLatestComment(reverse = false) {
	waitForElement(  //getting the menu
		getMenuButtonXPath(),
		(element) => {
			if (settings.isScroll) {
				element.scrollIntoView({ behavior: settings.scrollBehavior, block: "center" });
			}
			setTimeout(() => {
				safeClick(element);
			}, 100);
			waitForElement(  //getting the items in menu
				"//*[@role='menuitem']",
				(element) => {
					const menuItems = document.querySelectorAll('*[role="menuitem"]');
					if (menuItems.length > 1) {
						safeClick(menuItems[menuItems.length - 2]);
						if (settings.notifyEnabled) {
							notifyUser(reverse);
						}
					}
				}
			);
		}
	);
}

/**
 * bind the event for detected object after the DOM changes
 */

function bindForDetected() {
	let commentRightBottomBtn = document.querySelectorAll("div[role='button'][tabindex='0'][id^=':']");
	let commentBtn = document.querySelectorAll("span[data-ad-rendering-role='comment_button']");
	commentRightBottomBtn.forEach((btn) => {
		btn.addEventListener("click", handleClickOutside);
	});
	commentBtn.forEach((btn) => {
		btn.parentElement.parentElement.parentElement.addEventListener("click", handleClickOutside);
	});
}

/**
 * default settings
 */

const settings = {
	autoDetect: true, // auto detect all/latest comments
	scrollBehavior: "smooth", // scroll behavior
	notifyEnabled: true, // notify after action
	isAll: true, // all comments/latest comments
	isHidden: false, // settings panel hidden
	isScroll: false, // enable scroll effect
};

/**
 * save settings to gm storage
 */

async function saveSettings() {
	await GM.setValue("fbAllCommentsHelperSettings", JSON.stringify(settings));
}

/**
 * load settings from gm storage
 */

async function loadSettings() {
	const storedSettings = await GM.getValue("fbAllCommentsHelperSettings");
	if (storedSettings) {
		Object.assign(settings, JSON.parse(storedSettings));
	}
}

/**
 * create settings panel
 */

function createSettingsPanel() {
	const panel = document.createElement("div");
	panel.id = "settingsPanel";
	panel.style.position = "fixed";
	panel.style.top = "10px";
	panel.style.right = "10px";
	panel.style.backgroundColor = "rgba(0, 0, 0, 1)";
	panel.style.padding = "10px";
	panel.style.borderRadius = "5px";
	panel.style.zIndex = "9999";
	panel.style.fontSize = "14px";

	panel.innerHTML = `
        <h4 style="margin: 0 0 10px;color: white;">${getSettingsStr().settings}</h4>
        <label style="color: white;">
            <input type="checkbox" id="autoDetect" ${settings.autoDetect ? "checked" : ""}>
            ${getSettingsStr().autoDetect}
        </label><br>
        <label style="color: white;">
            <input type="checkbox" id="notifyEnabled" ${settings.notifyEnabled ? "checked" : ""}>
            ${getSettingsStr().notifyEnabled}
        </label><br>
		<label style="color: white;">
            <input type="checkbox" id="isAll" ${settings.isAll ? "checked" : ""}>
            ${getSettingsStr().isAll}
        </label><br>
		<label style="color: white;">
            <input type="checkbox" id="isScroll" ${settings.isScroll ? "checked" : ""}>
            ${getSettingsStr().isScroll}
        </label><br>
        <label style="color: white;">
		${getSettingsStr().scrollBehavior}
            <select id="scrollBehavior">
                <option value="smooth" ${settings.scrollBehavior === "smooth" ? "selected" : ""}>${getSettingsStr().smooth}</option>
                <option value="auto" ${settings.scrollBehavior === "auto" ? "selected" : ""}>${getSettingsStr().auto}</option>
            </select>
        </label><br>
        <button id="hideSettings" style="margin-top: 10px;">${getSettingsStr().hideSettings}</button>
    `;

	document.body.appendChild(panel);

	// add panel buttons event listeners
	document.getElementById("autoDetect").addEventListener("change", (e) => {
		settings.autoDetect = e.target.checked;
		saveSettings();
		alert(getSettingsStr().needRefresh);
	});
	document.getElementById("notifyEnabled").addEventListener("change", (e) => {
		settings.notifyEnabled = e.target.checked;
		saveSettings();
	});
	document.getElementById("isAll").addEventListener("change", (e) => {
		settings.isAll = e.target.checked;
		saveSettings();
	});
	document.getElementById("isScroll").addEventListener("change", (e) => {
		settings.isScroll = e.target.checked;
		saveSettings();
	});
	document.getElementById("scrollBehavior").addEventListener("change", (e) => {
		settings.scrollBehavior = e.target.value;
		saveSettings();
	});
	document.getElementById("hideSettings").addEventListener("click", () => {
		settings.isHidden = true;
		document.getElementById("settingsPanel").remove();
		saveSettings();
	});
}

(async function () {
	"use strict";
	window.addEventListener("load", async () => {
		await loadSettings();
		await saveSettings();
		await GM_registerMenuCommand(getSettingsStr().openMenu, () => {  // gm menu command
			createSettingsPanel();
			settings.isHidden = false;
		});
		if (!settings.isHidden) { // create settings panel when isHidden is false
			createSettingsPanel();
		}
		document.addEventListener("dblclick", actionParser);  //handle dblclick event
		document.addEventListener("keydown", actionParser);	//handle keydown event
		bindForDetected();
		if (settings.autoDetect) {  // for auto detect
			observeDOM(bindForDetected);
		}
	});
})();