FaceBook 贴文悬浮截图按钮

在贴文右上新增一个悬浮截图按钮,按下后可以对贴文进行截图保存,方便与其他人分享

当前为 2025-06-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Floating Screenshot Button for Facebook Posts
  3. // @name:zh-TW FaceBook 貼文懸浮截圖按鈕
  4. // @name:zh-CN FaceBook 贴文悬浮截图按钮
  5. // @namespace http://tampermonkey.net/
  6. // @version 2.1
  7. // @description A floating screenshot button is added to the top-right corner of the post. When clicked, it allows users to capture and save a screenshot of the post, making it easier to share with others.
  8. // @description:zh-TW 在貼文右上新增一個懸浮截圖按鈕,按下後可以對貼文進行截圖保存,方便與其他人分享
  9. // @description:zh-CN 在贴文右上新增一个悬浮截图按钮,按下后可以对贴文进行截图保存,方便与其他人分享
  10. // @author chatgpt
  11. // @match https://www.facebook.com/*
  12. // @grant none
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. let lastRun = 0; // 上次執行時間,用於防抖動
  21. const debounceDelay = 1000; // 防抖間隔時間(毫秒)
  22.  
  23. // 從貼文元素中取得 FB 貼文 ID
  24. function getFbidFromPost(post) {
  25. // 嘗試從貼文中超連結擷取 fbid 或 story_fbid
  26. const links = Array.from(post.querySelectorAll('a[href*="fbid="], a[href*="story_fbid="]'));
  27. for (const a of links) {
  28. try {
  29. const url = new URL(a.href);
  30. const fbid = url.searchParams.get('fbid') || url.searchParams.get('story_fbid'); // 從網址參數取得 ID
  31. if (fbid) return fbid;
  32. } catch (e) {}
  33. }
  34.  
  35. // 嘗試從 data-ft 屬性字串中用正則抓 top_level_post_id
  36. try {
  37. const dataFt = post.getAttribute('data-ft');
  38. if (dataFt) {
  39. const match = dataFt.match(/"top_level_post_id":"(\d+)"/);
  40. if (match) return match[1];
  41. }
  42. } catch (e) {}
  43.  
  44. // 嘗試從當前頁面網址抓 fbid 或 story_fbid 參數
  45. try {
  46. const url = new URL(window.location.href);
  47. const fbid = url.searchParams.get('fbid') || url.searchParams.get('story_fbid');
  48. if (fbid) return fbid;
  49. } catch (e) {}
  50.  
  51. return 'unknownFBID'; // 如果全部失敗,回傳預設值
  52. }
  53.  
  54. // 監聽頁面 DOM 變動,動態新增截圖按鈕
  55. const observer = new MutationObserver(() => {
  56. const now = Date.now();
  57. if (now - lastRun < debounceDelay) return; // 防抖
  58. lastRun = now;
  59.  
  60. // 尋找所有貼文容器
  61. document.querySelectorAll('div.x1lliihq').forEach(post => {
  62. if (post.dataset.sbtn === '1') return; // 已加入按鈕就跳過
  63.  
  64. // 找貼文中的按鈕群組容器
  65. let btnGroup = post.querySelector('div[role="group"]')
  66. || post.querySelector('div.xqcrz7y')
  67. || post.querySelector('div.x1qx5ct2');
  68. if (!btnGroup) return; // 找不到按鈕群組就跳過
  69.  
  70. post.dataset.sbtn = '1'; // 標記已加按鈕
  71. btnGroup.style.position = 'relative'; // 設相對定位,方便絕對定位按鈕
  72.  
  73. // 建立截圖按鈕
  74. const btn = document.createElement('div');
  75. btn.textContent = '📸'; // 按鈕文字(相機圖示)
  76. btn.title = '截圖貼文'; // 提示文字
  77. Object.assign(btn.style, { // 設定按鈕樣式
  78. position: 'absolute',
  79. left: '-40px',
  80. top: '0',
  81. width: '32px',
  82. height: '32px',
  83. display: 'flex',
  84. alignItems: 'center',
  85. justifyContent: 'center',
  86. borderRadius: '50%',
  87. backgroundColor: '#3A3B3C',
  88. color: 'white',
  89. cursor: 'pointer',
  90. zIndex: '9999',
  91. transition: 'background .2s'
  92. });
  93.  
  94. // 滑鼠移入變色
  95. btn.addEventListener('mouseenter', () => btn.style.backgroundColor = '#4E4F50');
  96. // 滑鼠移出還原顏色
  97. btn.addEventListener('mouseleave', () => btn.style.backgroundColor = '#3A3B3C');
  98.  
  99. // 按鈕點擊事件,進行截圖
  100. btn.addEventListener('click', async e => {
  101. e.stopPropagation(); // 防止事件冒泡
  102. btn.textContent = '⏳'; // 顯示等待中圖示
  103. btn.style.pointerEvents = 'none'; // 禁用按鈕避免重複點擊
  104.  
  105. try {
  106. if (!window.html2canvas) throw new Error('html2canvas 尚未載入');
  107.  
  108. // 嘗試展開「查看更多」內容
  109. const seeMoreCandidates = post.querySelectorAll('span, a, div, button');
  110. let clicked = false;
  111.  
  112. for (const el of seeMoreCandidates) {
  113. const text = el.innerText?.trim() || el.textContent?.trim();
  114. if (!text) continue;
  115.  
  116. if (
  117. text.includes('查看更多') ||
  118. text.includes('See more') || text.includes('See More') ||
  119. text.includes('…更多') || text.includes('more')
  120. ) {
  121. try {
  122. el.click();
  123. clicked = true;
  124. console.log('已點擊查看更多:', el);
  125. } catch (err) {
  126. console.warn('點擊查看更多失敗:', err);
  127. }
  128. }
  129. }
  130.  
  131. if (clicked) {
  132. await new Promise(r => setTimeout(r, 1000)); // 等待展開完成
  133. }
  134.  
  135. // 滾動到貼文位置
  136. post.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 滾動貼文至畫面中央
  137. await new Promise(r => setTimeout(r, 500)); // 等待 0.5 秒讓畫面穩定
  138.  
  139. // 建立檔案名稱
  140. const fbid = getFbidFromPost(post); // 取得貼文 ID
  141. const nowDate = new Date();
  142. const pad = n => n.toString().padStart(2, '0');
  143. const datetimeStr =
  144. nowDate.getFullYear().toString() +
  145. pad(nowDate.getMonth() + 1) +
  146. pad(nowDate.getDate()) + '_' +
  147. pad(nowDate.getHours()) + '_' +
  148. pad(nowDate.getMinutes()) + '_' +
  149. pad(nowDate.getSeconds());
  150.  
  151. // 使用 html2canvas 進行截圖
  152. const canvas = await html2canvas(post, {
  153. useCORS: true,
  154. allowTaint: true,
  155. backgroundColor: '#1c1c1d', //背景色設定,null 透明,'white' 白,'black' 黑
  156. scale: 2, //畫質設定,建議使用2,或者window.devicePixelRatio
  157. logging: false,
  158. foreignObjectRendering: false
  159. });
  160.  
  161. // 建立下載連結並觸發下載
  162. const filename = `${fbid}_${datetimeStr}.png`; // 檔名格式:FBID_時間.png
  163. const link = document.createElement('a');
  164. link.href = canvas.toDataURL('image/png');
  165. link.download = filename;
  166. link.click();
  167.  
  168. btn.textContent = '✅'; // 成功顯示勾勾
  169. } catch (err) {
  170. console.error('截圖錯誤:', err); // 錯誤輸出到 console
  171. alert('截圖失敗,請稍後再試'); // 跳出錯誤提示
  172. btn.textContent = '❌'; // 顯示錯誤圖示
  173. }
  174.  
  175. // 1秒後還原按鈕狀態
  176. setTimeout(() => {
  177. btn.textContent = '📸';
  178. btn.style.pointerEvents = 'auto';
  179. }, 1000);
  180. });
  181.  
  182. btnGroup.appendChild(btn); // 將按鈕加入按鈕群組
  183. });
  184. });
  185.  
  186. // 監聽整個 body 的 DOM 變化
  187. observer.observe(document.body, { childList: true, subtree: true });
  188.  
  189. })();