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.3
  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. * get the language of the fb
  208. * @returns the language of the fb
  209. */
  210. function detectLang() {
  211. return document.getElementById("facebook")?.getAttribute("lang") || "en";
  212. }
  213.  
  214. /**
  215. * get the settings string array
  216. * @returns the settings string array
  217. */
  218.  
  219. function getSettingsStr() {
  220. return settingsStr[detectLang()] || settingsStr.en;
  221. }
  222.  
  223. /**
  224. * get the xpath for menu
  225. * @returns the xpath for menu
  226. */
  227.  
  228. function getMenuButtonXPath() {
  229. const lang = langs[detectLang()] || langs.en;
  230. 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]}')]`;
  231. }
  232.  
  233. /**
  234. * handle the click the comment button or the right bottom comment count
  235. */
  236.  
  237. function handleClickOutside() {
  238. if (settings.isAll) showAllComment();
  239. else showLatestComment();
  240. }
  241.  
  242. /**
  243. * wait for the element to appear in the DOM
  244. * @param {string} xpath
  245. * @param {Function} callback
  246. * @param {number} timeout
  247. * @param {number} interval
  248. */
  249.  
  250. function waitForElement(xpath, callback, timeout = 3000, intervalTime = 100) {
  251. const startTime = Date.now();
  252. const interval = setInterval(() => {
  253. const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  254. if (element) {
  255. clearInterval(interval);
  256. callback(element);
  257. } else if (Date.now() - startTime > timeout) {
  258. clearInterval(interval);
  259. console.warn("Timeout: Element not found for XPath", xpath);
  260. }
  261. }, intervalTime);
  262. }
  263.  
  264. /**
  265. *
  266. * @param {Element} element
  267. */
  268.  
  269. function safeClick(element) {
  270. try {
  271. element.click();
  272. } catch (err) {
  273. console.error("Error clicking element:", err);
  274. }
  275. }
  276.  
  277. /**
  278. * show the notification to user after the action
  279. * override is for mode in settings is allcomments and user press ctrl+dblclick/ctrl+insert
  280. * @param {boolean} reverse
  281. */
  282.  
  283. async function notifyUser(reverse = false) {
  284. const notification = document.createElement("div");
  285. notification.setAttribute("id", "FBAllCommentsHelperNotification");
  286. notification.style.position = "fixed";
  287. notification.style.bottom = "20px";
  288. notification.style.left = "20px";
  289. notification.style.backgroundColor = "rgba(0,0,0,1)";
  290. notification.style.color = "white";
  291. notification.style.padding = "10px";
  292. notification.style.borderRadius = "5px";
  293. notification.style.zIndex = "9999";
  294. const notifyingTimeout = Number(await GM.getValue("notifyingTimeout"));
  295. if (reverse) {
  296. notification.textContent = notificationStr[detectLang()][settings.isAll ? 1 : 0] || notificationStr.en[settings.isAll ? 1 : 0];
  297. } else {
  298. notification.textContent = notificationStr[detectLang()][settings.isAll ? 0 : 1] || notificationStr.en[settings.isAll ? 0 : 1];
  299. }
  300. document.body.appendChild(notification);
  301. if (notifyingTimeout) {
  302. document.getElementById("FBAllCommentsHelperNotification").remove();
  303. clearTimeout(notifyingTimeout);
  304. }
  305. const id = setTimeout(async () => {
  306. notification.remove();
  307. await GM.deleteValue("notifyingTimeout");
  308. }, 3000);
  309. await GM.setValue("notifyingTimeout", id);
  310. }
  311.  
  312. /**
  313. * parse the user action
  314. * @param {*} e
  315. * @returns
  316. */
  317.  
  318. function actionParser(e) {
  319. if (e.type === "dblclick" && e.ctrlKey) {
  320. !settings.isAll ? showAllComment(true) : showLatestComment(true);
  321. return;
  322. }
  323. if (e.type === "dblclick") {
  324. settings.isAll ? showAllComment() : showLatestComment();
  325. return;
  326. }
  327. if (e.code === "Insert" && e.ctrlKey) {
  328. !settings.isAll ? showAllComment(true) : showLatestComment(true);
  329. return;
  330. }
  331. if (e.code === "Insert") {
  332. settings.isAll ? showAllComment() : showLatestComment();
  333. return;
  334. }
  335. }
  336.  
  337. /**
  338. * detecting the changes in the DOM
  339. * @param {Function} callback
  340. */
  341.  
  342. function observeDOM(callback) {
  343. const observer = new MutationObserver((mutations) => {
  344. mutations.forEach(() => callback());
  345. });
  346. observer.observe(document.body, { childList: true, subtree: true });
  347. }
  348.  
  349. /**
  350. * show all comments
  351. */
  352.  
  353. function showAllComment(reverse = false) {
  354. waitForElement(
  355. //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(
  365. //getting the items in menu
  366. "//*[@role='menuitem']",
  367. (element) => {
  368. const menuItems = document.querySelectorAll('*[role="menuitem"]');
  369. if (menuItems.length > 1) {
  370. safeClick(menuItems[menuItems.length - 1]);
  371. if (settings.notifyEnabled) {
  372. notifyUser(reverse);
  373. }
  374. }
  375. }
  376. );
  377. }
  378. );
  379. }
  380.  
  381. /**
  382. * show latest comment
  383. * override is for mode in settings is allcomments and user press ctrl+dblclick/ctrl+insert
  384. * @param {boolean} reverse
  385. */
  386. function showLatestComment(reverse = false) {
  387. waitForElement(
  388. //getting the menu
  389. getMenuButtonXPath(),
  390. (element) => {
  391. if (settings.isScroll) {
  392. element.scrollIntoView({ behavior: settings.scrollBehavior, block: "center" });
  393. }
  394. setTimeout(() => {
  395. safeClick(element);
  396. }, 100);
  397. waitForElement(
  398. //getting the items in menu
  399. "//*[@role='menuitem']",
  400. (element) => {
  401. const menuItems = document.querySelectorAll('*[role="menuitem"]');
  402. if (menuItems.length > 1) {
  403. safeClick(menuItems[menuItems.length - 2]);
  404. if (settings.notifyEnabled) {
  405. notifyUser(reverse);
  406. }
  407. }
  408. }
  409. );
  410. }
  411. );
  412. }
  413.  
  414. /**
  415. * bind the event for detected object after the DOM changes
  416. */
  417.  
  418. function bindForDetected() {
  419. let commentRightBottomBtn = document.querySelectorAll("div[role='button'][tabindex='0'][id^=':']");
  420. let commentBtn = document.querySelectorAll("span[data-ad-rendering-role='comment_button']");
  421. commentRightBottomBtn.forEach((btn) => {
  422. btn.addEventListener("click", handleClickOutside);
  423. });
  424. commentBtn.forEach((btn) => {
  425. btn.parentElement.parentElement.parentElement.addEventListener("click", handleClickOutside);
  426. });
  427. }
  428.  
  429. /**
  430. * default settings
  431. */
  432.  
  433. const settings = {
  434. autoDetect: true, // auto detect all/latest comments
  435. scrollBehavior: "smooth", // scroll behavior
  436. notifyEnabled: true, // notify after action
  437. isAll: true, // all comments/latest comments
  438. isHidden: false, // settings panel hidden
  439. isScroll: false, // enable scroll effect
  440. };
  441.  
  442. /**
  443. * save settings to gm storage
  444. */
  445.  
  446. async function saveSettings() {
  447. await GM.setValue("fbAllCommentsHelperSettings", JSON.stringify(settings));
  448. }
  449.  
  450. /**
  451. * load settings from gm storage
  452. */
  453.  
  454. async function loadSettings() {
  455. const storedSettings = await GM.getValue("fbAllCommentsHelperSettings");
  456. if (storedSettings) {
  457. Object.assign(settings, JSON.parse(storedSettings));
  458. }
  459. }
  460.  
  461. /**
  462. * create settings panel
  463. */
  464.  
  465. function createSettingsPanel() {
  466. const panel = document.createElement("div");
  467. panel.id = "settingsPanel";
  468. panel.style.position = "fixed";
  469. panel.style.top = "10px";
  470. panel.style.right = "10px";
  471. panel.style.backgroundColor = "rgba(0, 0, 0, 1)";
  472. panel.style.padding = "10px";
  473. panel.style.borderRadius = "5px";
  474. panel.style.zIndex = "9999";
  475. panel.style.fontSize = "14px";
  476.  
  477. panel.innerHTML = `
  478. <h4 style="margin: 0 0 10px;color: white;">${getSettingsStr().settings}</h4>
  479. <label style="color: white;">
  480. <input type="checkbox" id="autoDetect" ${settings.autoDetect ? "checked" : ""}>
  481. ${getSettingsStr().autoDetect}
  482. </label><br>
  483. <label style="color: white;">
  484. <input type="checkbox" id="notifyEnabled" ${settings.notifyEnabled ? "checked" : ""}>
  485. ${getSettingsStr().notifyEnabled}
  486. </label><br>
  487. <label style="color: white;">
  488. <input type="checkbox" id="isAll" ${settings.isAll ? "checked" : ""}>
  489. ${getSettingsStr().isAll}
  490. </label><br>
  491. <label style="color: white;">
  492. <input type="checkbox" id="isScroll" ${settings.isScroll ? "checked" : ""}>
  493. ${getSettingsStr().isScroll}
  494. </label><br>
  495. <label style="color: white;">
  496. ${getSettingsStr().scrollBehavior}
  497. <select id="scrollBehavior">
  498. <option value="smooth" ${settings.scrollBehavior === "smooth" ? "selected" : ""}>${getSettingsStr().smooth}</option>
  499. <option value="auto" ${settings.scrollBehavior === "auto" ? "selected" : ""}>${getSettingsStr().auto}</option>
  500. </select>
  501. </label><br>
  502. <button id="hideSettings" style="margin-top: 10px;">${getSettingsStr().hideSettings}</button>
  503. `;
  504.  
  505. document.body.appendChild(panel);
  506.  
  507. // add panel buttons event listeners
  508. document.getElementById("autoDetect").addEventListener("change", (e) => {
  509. settings.autoDetect = e.target.checked;
  510. saveSettings();
  511. alert(getSettingsStr().needRefresh);
  512. });
  513. document.getElementById("notifyEnabled").addEventListener("change", (e) => {
  514. settings.notifyEnabled = e.target.checked;
  515. saveSettings();
  516. });
  517. document.getElementById("isAll").addEventListener("change", (e) => {
  518. settings.isAll = e.target.checked;
  519. saveSettings();
  520. });
  521. document.getElementById("isScroll").addEventListener("change", (e) => {
  522. settings.isScroll = e.target.checked;
  523. saveSettings();
  524. });
  525. document.getElementById("scrollBehavior").addEventListener("change", (e) => {
  526. settings.scrollBehavior = e.target.value;
  527. saveSettings();
  528. });
  529. document.getElementById("hideSettings").addEventListener("click", () => {
  530. settings.isHidden = true;
  531. document.getElementById("settingsPanel").remove();
  532. saveSettings();
  533. });
  534. }
  535.  
  536. (async function () {
  537. "use strict";
  538. window.addEventListener("load", async () => {
  539. await loadSettings();
  540. await saveSettings();
  541. await GM_registerMenuCommand(getSettingsStr().openMenu, () => {
  542. // gm menu command
  543. createSettingsPanel();
  544. settings.isHidden = false;
  545. });
  546. if (!settings.isHidden) {
  547. // create settings panel when isHidden is false
  548. createSettingsPanel();
  549. }
  550. document.addEventListener("dblclick", actionParser); //handle dblclick event
  551. document.addEventListener("keydown", actionParser); //handle keydown event
  552. bindForDetected();
  553. if (settings.autoDetect) {
  554. // for auto detect
  555. observeDOM(bindForDetected);
  556. }
  557. });
  558. })();