FB全部留言小帮手

让您更快打开全部留言

目前为 2024-11-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Facebook All Comments Helper
  3. // @name:zh-TW FB全部留言小幫手
  4. // @name:zh-CN FB全部留言小帮手
  5. // @namespace http://tampermonkey.net/
  6. // @version 2.0
  7. // @description Easy way to show all comments.
  8. // @description:zh-tw 讓您更快打開全部留言
  9. // @description:zh-cn 让您更快打开全部留言
  10. // @author Xuitty
  11. // @match https://www.facebook.com/*
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=facebook.com
  13. // @grant GM_registerMenuCommand
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @run-at document-end
  17. // @license MIT
  18. // ==/UserScript==
  19.  
  20. /**
  21. * string array for detecting the menu button
  22. */
  23.  
  24. const langs = {
  25. de: ["Relevanteste", "Top-Kommentare", "Am zutreffendsten", "Neueste zuerst", "Neueste", "Alle Kommentare"],
  26. en: ["Top comments", "Most relevant", "Most applicable", "Most recent", "Newest", "All comments"],
  27. es: ["Comentarios destacados", "Más relevantes", "Más pertinentes", "Más recientes", "Más recientes", "Todos los comentarios"],
  28. 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"],
  29. ja: ["トップコメント", "関連度の高い順", "最も適切", "新しい順", "新しい順", "すべてのコメント"],
  30. ko: ["관련성 높은 댓글", "참여도 높은 댓글", "적합성 높은 순", "최신순", "날짜 내림차순", "모든 댓글"],
  31. fr: ["Plus pertinents", "Les meilleurs commentaires", "Les plus pertinents", "Plus récents", "Les plus récents", "Tous les commentaires"],
  32. sk: ["Top komentáre", "Najrelevantnejšie", "Najvhodnejšie", "Najnovšie", "Najnovšie", "Všetky komentáre"],
  33. sl: ["Najbolj priljubljeni komentarji", "Najustreznejši", "Najustreznejše", "Najnovejši", "Najnovejši", "Vsi komentarji"],
  34. "zh-Hans": ["热门评论", "最相关", "最合适", "从新到旧", "最新", "所有评论"],
  35. "zh-Hant": ["最熱門留言", "最相關", "最相關", "最新", "由新到舊", "所有留言"],
  36. };
  37.  
  38. /**
  39. * string array for notification
  40. */
  41.  
  42. const notificationStr = {
  43. de: ["Wechseln zu allen Kommentaren!", "Wechseln zu den neuesten Kommentaren!"],
  44. en: ["Switch to All Comments!", "Switch to Latest Comments!"],
  45. es: ["Cambiar a todos los comentarios!", "Cambiar a los comentarios más recientes!"],
  46. hu: ["Váltás az összes hozzászólásra!", "Váltás a legújabb hozzászólásokra!"],
  47. ja: ["すべてのコメントに切り替え!", "最新のコメントに切り替え!"],
  48. ko: ["모든 댓글로 전환!", "최신 댓글로 전환!"],
  49. fr: ["Passer à tous les commentaires!", "Passer aux commentaires les plus récents!"],
  50. sk: ["Prepnúť na všetky komentáre!", "Prepnúť na najnovšie komentáre!"],
  51. sl: ["Preklopite na vse komentarje!", "Preklopite na najnovejše komentarje!"],
  52. "zh-Hant": ["切換到所有留言!", "切換到最新留言!"],
  53. "zh-Hans": ["切换到所有评论!", "切换到最新评论!"],
  54. };
  55.  
  56. /**
  57. * string array for settings
  58. */
  59.  
  60. const settingsStr = {
  61. "zh-Hant": {
  62. settings: "設定",
  63. autoDetect: "自動切換為全部/最新留言",
  64. notifyEnabled: "操作通知",
  65. isAll: "全部留言/最新留言",
  66. isScroll: "是否開啟捲動",
  67. scrollBehavior: "捲動特效",
  68. hideSettings: "隱藏選單",
  69. smooth: "平滑",
  70. auto: "無",
  71. openMenu: "開啟選單",
  72. needRefresh: "需要重新整理頁面應用自動切換",
  73. },
  74. "zh-Hans": {
  75. settings: "设置",
  76. autoDetect: "自动切换为全部/最新评论",
  77. notifyEnabled: "操作通知",
  78. isAll: "全部评论/最新评论",
  79. isScroll: "是否开启滚动",
  80. scrollBehavior: "滚动特效",
  81. hideSettings: "隐藏菜单",
  82. smooth: "平滑",
  83. auto: "无",
  84. openMenu: "打开菜单",
  85. needRefresh: "需要重新刷新页面应用自动切换",
  86. },
  87. en: {
  88. settings: "Settings",
  89. autoDetect: "Auto detect all/latest comments",
  90. notifyEnabled: "Notify after action",
  91. isAll: "All comments/Latest comments",
  92. isScroll: "Enable scroll effect",
  93. scrollBehavior: "Scroll behavior",
  94. hideSettings: "Hide settings",
  95. smooth: "Smooth",
  96. auto: "None",
  97. openMenu: "Open menu",
  98. needRefresh: "Need to refresh the page to apply auto detection",
  99. },
  100. de: {
  101. settings: "Einstellungen",
  102. autoDetect: "Automatisch alle/neuesten Kommentare erkennen",
  103. notifyEnabled: "Nach Aktion benachrichtigen",
  104. isAll: "Alle Kommentare/Neueste Kommentare",
  105. isScroll: "Bildlauf aktivieren",
  106. scrollBehavior: "Bildlaufverhalten",
  107. hideSettings: "Einstellungen ausblenden",
  108. smooth: "Sanft",
  109. auto: "Keine",
  110. openMenu: "Menü öffnen",
  111. needRefresh: "Die Seite muss aktualisiert werden, um die automatische Erkennung anzuwenden",
  112. },
  113. es: {
  114. settings: "Ajustes",
  115. autoDetect: "Detectar automáticamente todos/los comentarios más recientes",
  116. notifyEnabled: "Notificar después de la acción",
  117. isAll: "Todos los comentarios/Comentarios más recientes",
  118. isScroll: "Activar efecto de desplazamiento",
  119. scrollBehavior: "Comportamiento de desplazamiento",
  120. hideSettings: "Ocultar ajustes",
  121. smooth: "Suave",
  122. auto: "Ninguno",
  123. openMenu: "Abrir menú",
  124. needRefresh: "Necesita actualizar la página para aplicar la detección automática",
  125. },
  126. fr: {
  127. settings: "Paramètres",
  128. autoDetect: "Détecter automatiquement tous/les derniers commentaires",
  129. notifyEnabled: "Notifier après l'action",
  130. isAll: "Tous les commentaires/Derniers commentaires",
  131. isScroll: "Activer l'effet de défilement",
  132. scrollBehavior: "Comportement de défilement",
  133. hideSettings: "Masquer les paramètres",
  134. smooth: "Doux",
  135. auto: "Aucun",
  136. openMenu: "Ouvrir le menu",
  137. needRefresh: "Besoin de rafraîchir la page pour appliquer la détection automatique",
  138. },
  139. hu: {
  140. settings: "Beállítások",
  141. autoDetect: "Az összes/legújabb hozzászólás automatikus észlelése",
  142. notifyEnabled: "Értesítés az akció után",
  143. isAll: "Minden megjegyzés/Legfrissebb megjegyzések",
  144. isScroll: "Gördítési hatás engedélyezése",
  145. scrollBehavior: "Gördülési viselkedés",
  146. hideSettings: "Beállítások elrejtése",
  147. smooth: "Simít",
  148. auto: "Nincs",
  149. openMenu: "Menü megnyitása",
  150. needRefresh: "Az automatikus észlelés alkalmazásához frissíteni kell az oldalt",
  151. },
  152. ja: {
  153. settings: "設定",
  154. autoDetect: "すべて/最新のコメントを自動検出",
  155. notifyEnabled: "アクション後に通知",
  156. isAll: "すべてのコメント/最新のコメント",
  157. isScroll: "スクロール効果を有効にする",
  158. scrollBehavior: "スクロール動作",
  159. hideSettings: "設定を非表示",
  160. smooth: "スムーズ",
  161. auto: "なし",
  162. openMenu: "メニューを開く",
  163. needRefresh: "自動検出を適用するにはページを更新する必要があります",
  164. },
  165. ko: {
  166. settings: "설정",
  167. autoDetect: "모든/최신 댓글 자동 감지",
  168. notifyEnabled: "작업 후 알림",
  169. isAll: "모든 댓글/최신 댓글",
  170. isScroll: "스크롤 효과 사용",
  171. scrollBehavior: "스크롤 동작",
  172. hideSettings: "설정 숨기기",
  173. smooth: "부드러운",
  174. auto: "없음",
  175. openMenu: "메뉴 열기",
  176. needRefresh: "자동 감지를 적용하려면 페이지를 새로 고쳐야 합니다",
  177. },
  178. sk: {
  179. settings: "Nastavenia",
  180. autoDetect: "Automaticky zistiť všetky/najnovšie komentáre",
  181. notifyEnabled: "Upozorniť po akcii",
  182. isAll: "Všetky komentáre/Najnovšie komentáre",
  183. isScroll: "Povoliť posuvný efekt",
  184. scrollBehavior: "Správanie posuvu",
  185. hideSettings: "Skryť nastavenia",
  186. smooth: "Hladký",
  187. auto: "Žiadny",
  188. openMenu: "Otvoriť menu",
  189. needRefresh: "Na použitie automatického zistenia je potrebné obnoviť stránku",
  190. },
  191. sl: {
  192. settings: "Nastavitve",
  193. autoDetect: "Samodejno zaznani vsi/najnovejši komentarji",
  194. notifyEnabled: "Obvesti po dejanju",
  195. isAll: "Vsi komentarji/Najnovejši komentarji",
  196. isScroll: "Omogoči učinek drsenja",
  197. scrollBehavior: "Vedenje drsenja",
  198. hideSettings: "Skrij nastavitve",
  199. smooth: "Gladko",
  200. auto: "Brez",
  201. openMenu: "Odpri meni",
  202. needRefresh: "Za uporabo samodejnega zaznavanja je treba osvežiti stran",
  203. },
  204. };
  205.  
  206.  
  207. /**
  208. * get the language of the fb
  209. * @returns the language of the fb
  210. */
  211. function detectLang() {
  212. return document.getElementById("facebook")?.getAttribute("lang") || "en";
  213. }
  214.  
  215. /**
  216. * get the settings string array
  217. * @returns the settings string array
  218. */
  219.  
  220. function getSettingsStr() {
  221. return settingsStr[detectLang()] || settingsStr.en;
  222. }
  223.  
  224. /**
  225. * get the xpath for menu
  226. * @returns the xpath for menu
  227. */
  228.  
  229. function getMenuButtonXPath() {
  230. const lang = langs[detectLang()] || langs.en;
  231. 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]}')]`;
  232. }
  233.  
  234. /**
  235. * handle the click the comment button or the right bottom comment count
  236. */
  237.  
  238. function handleClickOutside() {
  239. if (settings.isAll) showAllComment();
  240. else showLatestComment();
  241. }
  242.  
  243. /**
  244. * wait for the element to appear in the DOM
  245. * @param {string} xpath
  246. * @param {Function} callback
  247. * @param {number} timeout
  248. * @param {number} interval
  249. */
  250.  
  251. function waitForElement(xpath, callback, timeout = 3000, intervalTime = 100) {
  252. const startTime = Date.now();
  253. const interval = setInterval(() => {
  254. const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  255. if (element) {
  256. clearInterval(interval);
  257. callback(element);
  258. } else if (Date.now() - startTime > timeout) {
  259. clearInterval(interval);
  260. console.warn("Timeout: Element not found for XPath", xpath);
  261. }
  262. }, intervalTime);
  263. }
  264.  
  265. /**
  266. *
  267. * @param {Element} element
  268. */
  269.  
  270. function safeClick(element) {
  271. try {
  272. element.click();
  273. } catch (err) {
  274. console.error("Error clicking element:", err);
  275. }
  276. }
  277.  
  278. /**
  279. * show the notification to user after the action
  280. * override is for mode in settings is allcomments and user press ctrl+dblclick/ctrl+insert
  281. * @param {boolean} reverse
  282. */
  283.  
  284. async function notifyUser(reverse = false) {
  285. const notification = document.createElement("div");
  286. notification.setAttribute("id", "FBAllCommentsHelperNotification");
  287. notification.style.position = "fixed";
  288. notification.style.bottom = "20px";
  289. notification.style.left = "20px";
  290. notification.style.backgroundColor = "rgba(0,0,0,1)";
  291. notification.style.color = "white";
  292. notification.style.padding = "10px";
  293. notification.style.borderRadius = "5px";
  294. notification.style.zIndex = "9999";
  295. const notifyingTimeout = (Number) (await GM.getValue("notifyingTimeout"));
  296. if (reverse) {
  297. notification.textContent = notificationStr[detectLang()][settings.isAll ? 1 : 0] || notificationStr.en[settings.isAll ? 1 : 0];
  298. } else {
  299. notification.textContent = notificationStr[detectLang()][settings.isAll ? 0 : 1] || notificationStr.en[settings.isAll ? 0 : 1];
  300. }
  301. document.body.appendChild(notification);
  302. if (notifyingTimeout) {
  303. document.getElementById("FBAllCommentsHelperNotification").remove();
  304. clearTimeout(notifyingTimeout);
  305. };
  306. const id = setTimeout(async () => {
  307. notification.remove();
  308. await GM.deleteValue("notifyingTimeout");
  309. }, 3000);
  310. await GM.setValue("notifyingTimeout", id);
  311. }
  312.  
  313. /**
  314. * parse the user action
  315. * @param {*} e
  316. * @returns
  317. */
  318.  
  319. function actionParser(e) {
  320. if (e.type === "dblclick" && e.ctrlKey) {
  321. !settings.isAll ? showAllComment(true) : showLatestComment(true);
  322. return;
  323. }
  324. if (e.type === "dblclick") {
  325. settings.isAll ? showAllComment() : showLatestComment();
  326. return;
  327. }
  328. if (e.code === "Insert" && e.ctrlKey) {
  329. !settings.isAll ? showAllComment(true) : showLatestComment(true);
  330. return;
  331. }
  332. if (e.code === "Insert") {
  333. settings.isAll ? showAllComment() : showLatestComment();
  334. return;
  335. }
  336. }
  337.  
  338. /**
  339. * detecting the changes in the DOM
  340. * @param {Function} callback
  341. */
  342.  
  343. function observeDOM(callback) {
  344. const observer = new MutationObserver((mutations) => {
  345. mutations.forEach(() => callback());
  346. });
  347. observer.observe(document.body, { childList: true, subtree: true });
  348. }
  349.  
  350. /**
  351. * show all comments
  352. */
  353.  
  354. function showAllComment(reverse = false) {
  355. waitForElement( //getting the menu
  356. getMenuButtonXPath(),
  357. (element) => {
  358. if (settings.isScroll) {
  359. element.scrollIntoView({ behavior: settings.scrollBehavior, block: "center" });
  360. }
  361. setTimeout(() => {
  362. safeClick(element);
  363. }, 100);
  364. waitForElement( //getting the items in menu
  365. "//*[@role='menuitem']",
  366. (element) => {
  367. const menuItems = document.querySelectorAll('*[role="menuitem"]');
  368. if (menuItems.length > 1) {
  369. safeClick(menuItems[menuItems.length - 1]);
  370. if (settings.notifyEnabled) {
  371. notifyUser(reverse);
  372. }
  373. }
  374. }
  375. );
  376. }
  377. );
  378. }
  379.  
  380. /**
  381. * show latest comment
  382. * override is for mode in settings is allcomments and user press ctrl+dblclick/ctrl+insert
  383. * @param {boolean} reverse
  384. */
  385. function showLatestComment(reverse = false) {
  386. waitForElement( //getting the menu
  387. getMenuButtonXPath(),
  388. (element) => {
  389. if (settings.isScroll) {
  390. element.scrollIntoView({ behavior: settings.scrollBehavior, block: "center" });
  391. }
  392. setTimeout(() => {
  393. safeClick(element);
  394. }, 100);
  395. waitForElement( //getting the items in menu
  396. "//*[@role='menuitem']",
  397. (element) => {
  398. const menuItems = document.querySelectorAll('*[role="menuitem"]');
  399. if (menuItems.length > 1) {
  400. safeClick(menuItems[menuItems.length - 2]);
  401. if (settings.notifyEnabled) {
  402. notifyUser(reverse);
  403. }
  404. }
  405. }
  406. );
  407. }
  408. );
  409. }
  410.  
  411. /**
  412. * bind the event for detected object after the DOM changes
  413. */
  414.  
  415. function bindForDetected() {
  416. let commentRightBottomBtn = document.querySelectorAll("div[role='button'][tabindex='0'][id^=':']");
  417. let commentBtn = document.querySelectorAll("span[data-ad-rendering-role='comment_button']");
  418. commentRightBottomBtn.forEach((btn) => {
  419. btn.addEventListener("click", handleClickOutside);
  420. });
  421. commentBtn.forEach((btn) => {
  422. btn.parentElement.parentElement.parentElement.addEventListener("click", handleClickOutside);
  423. });
  424. }
  425.  
  426. /**
  427. * default settings
  428. */
  429.  
  430. const settings = {
  431. autoDetect: true, // auto detect all/latest comments
  432. scrollBehavior: "smooth", // scroll behavior
  433. notifyEnabled: true, // notify after action
  434. isAll: true, // all comments/latest comments
  435. isHidden: false, // settings panel hidden
  436. isScroll: false, // enable scroll effect
  437. };
  438.  
  439. /**
  440. * save settings to gm storage
  441. */
  442.  
  443. async function saveSettings() {
  444. await GM.setValue("fbAllCommentsHelperSettings", JSON.stringify(settings));
  445. }
  446.  
  447. /**
  448. * load settings from gm storage
  449. */
  450.  
  451. async function loadSettings() {
  452. const storedSettings = await GM.getValue("fbAllCommentsHelperSettings");
  453. if (storedSettings) {
  454. Object.assign(settings, JSON.parse(storedSettings));
  455. }
  456. }
  457.  
  458. /**
  459. * create settings panel
  460. */
  461.  
  462. function createSettingsPanel() {
  463. const panel = document.createElement("div");
  464. panel.id = "settingsPanel";
  465. panel.style.position = "fixed";
  466. panel.style.top = "10px";
  467. panel.style.right = "10px";
  468. panel.style.backgroundColor = "rgba(0, 0, 0, 1)";
  469. panel.style.padding = "10px";
  470. panel.style.borderRadius = "5px";
  471. panel.style.zIndex = "9999";
  472. panel.style.fontSize = "14px";
  473.  
  474. panel.innerHTML = `
  475. <h4 style="margin: 0 0 10px;color: white;" id="settingsPanel">${getSettingsStr().settings}</h4>
  476. <label style="color: white;">
  477. <input type="checkbox" id="autoDetect" ${settings.autoDetect ? "checked" : ""}>
  478. ${getSettingsStr().autoDetect}
  479. </label><br>
  480. <label style="color: white;">
  481. <input type="checkbox" id="notifyEnabled" ${settings.notifyEnabled ? "checked" : ""}>
  482. ${getSettingsStr().notifyEnabled}
  483. </label><br>
  484. <label style="color: white;">
  485. <input type="checkbox" id="isAll" ${settings.isAll ? "checked" : ""}>
  486. ${getSettingsStr().isAll}
  487. </label><br>
  488. <label style="color: white;">
  489. <input type="checkbox" id="isScroll" ${settings.isScroll ? "checked" : ""}>
  490. ${getSettingsStr().isScroll}
  491. </label><br>
  492. <label style="color: white;">
  493. ${getSettingsStr().scrollBehavior}
  494. <select id="scrollBehavior">
  495. <option value="smooth" ${settings.scrollBehavior === "smooth" ? "selected" : ""}>${getSettingsStr().smooth}</option>
  496. <option value="auto" ${settings.scrollBehavior === "auto" ? "selected" : ""}>${getSettingsStr().auto}</option>
  497. </select>
  498. </label><br>
  499. <button id="hideSettings" style="margin-top: 10px;">${getSettingsStr().hideSettings}</button>
  500. `;
  501.  
  502. document.body.appendChild(panel);
  503.  
  504. // add panel buttons event listeners
  505. document.getElementById("autoDetect").addEventListener("change", (e) => {
  506. settings.autoDetect = e.target.checked;
  507. saveSettings();
  508. alert(getSettingsStr().needRefresh);
  509. });
  510. document.getElementById("notifyEnabled").addEventListener("change", (e) => {
  511. settings.notifyEnabled = e.target.checked;
  512. saveSettings();
  513. });
  514. document.getElementById("isAll").addEventListener("change", (e) => {
  515. settings.isAll = e.target.checked;
  516. saveSettings();
  517. });
  518. document.getElementById("isScroll").addEventListener("change", (e) => {
  519. settings.isScroll = e.target.checked;
  520. saveSettings();
  521. });
  522. document.getElementById("scrollBehavior").addEventListener("change", (e) => {
  523. settings.scrollBehavior = e.target.value;
  524. saveSettings();
  525. });
  526. document.getElementById("hideSettings").addEventListener("click", () => {
  527. settings.isHidden = true;
  528. document.getElementById("settingsPanel").remove();
  529. saveSettings();
  530. });
  531. }
  532.  
  533. (async function () {
  534. "use strict";
  535. window.addEventListener("load", async () => {
  536. await loadSettings();
  537. await saveSettings();
  538. await GM_registerMenuCommand(getSettingsStr().openMenu, () => { // gm menu command
  539. createSettingsPanel();
  540. settings.isHidden = false;
  541. });
  542. if (!settings.isHidden) { // create settings panel when isHidden is false
  543. createSettingsPanel();
  544. }
  545. document.addEventListener("dblclick", actionParser); //handle dblclick event
  546. document.addEventListener("keydown", actionParser); //handle keydown event
  547. if (settings.autoDetect) { // for auto detect
  548. observeDOM(bindForDetected);
  549. }
  550. });
  551. })();