FaceBook 貼文懸浮截圖按鈕

在貼文右上新增一個懸浮截圖按鈕,按下後可以對貼文進行截圖保存,方便與其他人分享

目前為 2025-06-08 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Floating Screenshot Button for Facebook Posts
// @name:zh-TW   FaceBook 貼文懸浮截圖按鈕
// @name:zh-CN   FaceBook 贴文悬浮截图按钮
// @namespace    http://tampermonkey.net/
// @version      1.6
// @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.
// @description:zh-TW 在貼文右上新增一個懸浮截圖按鈕,按下後可以對貼文進行截圖保存,方便與其他人分享
// @description:zh-CN 在贴文右上新增一个悬浮截图按钮,按下后可以对贴文进行截图保存,方便与其他人分享
// @author       chatgpt
// @match        https://www.facebook.com/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @license MIT
// ==/UserScript==

(function() {
  'use strict';

  let lastRun = 0;                    // 上次執行時間,用於防抖動
  const debounceDelay = 1000;        // 防抖動延遲,避免過度頻繁執行

  // 從貼文元素中取得 FB 貼文 ID
  function getFbidFromPost(post) {
    // 嘗試從貼文內含 fbid 或 story_fbid 的連結抓取
    const links = Array.from(post.querySelectorAll('a[href*="fbid="], a[href*="story_fbid="]'));
    for (const a of links) {
      try {
        const url = new URL(a.href);
        const fbid = url.searchParams.get('fbid') || url.searchParams.get('story_fbid');  // 從網址參數取得 ID
        if (fbid) return fbid;
      } catch(e) {}
    }

    // 嘗試從 data-ft 屬性字串中用正則抓 top_level_post_id
    try {
      const dataFt = post.getAttribute('data-ft');
      if (dataFt) {
        const match = dataFt.match(/"top_level_post_id":"(\d+)"/);
        if (match) return match[1];
      }
    } catch(e) {}

    // 嘗試從當前頁面網址抓 fbid 或 story_fbid 參數
    try {
      const url = new URL(window.location.href);
      const fbid = url.searchParams.get('fbid') || url.searchParams.get('story_fbid');
      if (fbid) return fbid;
    } catch(e) {}

    return 'unknownFBID';            // 都抓不到時回傳預設字串
  }

  // 監聽頁面 DOM 變動,動態新增截圖按鈕
  const observer = new MutationObserver(() => {
    const now = Date.now();
    if (now - lastRun < debounceDelay) return;  // 防抖,避免頻繁觸發
    lastRun = now;

    // 找所有貼文容器元素
    document.querySelectorAll('div.x1lliihq').forEach(post => {
      if (post.dataset.sbtn === '1') return;     // 已加入按鈕就跳過

      // 找貼文中的按鈕群組容器
      let btnGroup = post.querySelector('div[role="group"]')
                  || post.querySelector('div.xqcrz7y')
                  || post.querySelector('div.x1qx5ct2');
      if (!btnGroup) return;                     // 找不到按鈕群組就跳過

      post.dataset.sbtn = '1';                   // 標記已加按鈕
      btnGroup.style.position = 'relative';      // 設相對定位,方便絕對定位按鈕

      // 建立截圖按鈕
      const btn = document.createElement('div');
      btn.textContent = '📸';                    // 按鈕文字(相機圖示)
      btn.title = '截圖貼文';                     // 提示文字
      Object.assign(btn.style, {                  // 設定按鈕樣式
        position: 'absolute',
        left: '-40px',
        top: '0',
        width: '32px',
        height: '32px',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        borderRadius: '50%',
        backgroundColor: '#3A3B3C',
        color: 'white',
        cursor: 'pointer',
        zIndex: '9999',
        transition: 'background .2s'
      });
      // 滑鼠移入變色
      btn.addEventListener('mouseenter', () => btn.style.backgroundColor = '#4E4F50');
      // 滑鼠移出還原顏色
      btn.addEventListener('mouseleave', () => btn.style.backgroundColor = '#3A3B3C');

      // 按鈕點擊事件,進行截圖
      btn.addEventListener('click', async e => {
        e.stopPropagation();                     // 防止事件冒泡
        btn.textContent = '⏳';                 // 顯示等待中圖示
        btn.style.pointerEvents = 'none';        // 禁用按鈕避免重複點擊

        try {
          if (!window.html2canvas) throw new Error('html2canvas 尚未載入');

          post.scrollIntoView({ behavior: 'smooth', block: 'center' });  // 滾動貼文至畫面中央
          await new Promise(r => setTimeout(r, 500));                    // 等待 0.5 秒讓畫面穩定

          const fbid = getFbidFromPost(post);    // 取得貼文 ID

          // 產生時間字串,用於檔名
          const nowDate = new Date();
          const pad = n => n.toString().padStart(2, '0');
          const datetimeStr =
            nowDate.getFullYear().toString() +
            pad(nowDate.getMonth() + 1) +
            pad(nowDate.getDate()) + '_' +
            pad(nowDate.getHours()) + '_' +
            pad(nowDate.getMinutes()) + '_' +
            pad(nowDate.getSeconds());

          // 使用 html2canvas 將貼文轉成 canvas
          const canvas = await html2canvas(post, {
            useCORS: true,
            allowTaint: true,
            backgroundColor: '#fff',
            scale: window.devicePixelRatio,
            logging: false,
            foreignObjectRendering: false
          });

          const filename = `${fbid}_${datetimeStr}.png`;  // 檔名格式:FBID_時間.png

          // 建立下載連結並觸發下載
          const link = document.createElement('a');
          link.href = canvas.toDataURL('image/png');
          link.download = filename;
          link.click();

          btn.textContent = '✅';                // 成功顯示勾勾
        } catch (err) {
          console.error('截圖錯誤:', err);       // 錯誤輸出到 console
          alert('截圖失敗,請稍後再試');          // 跳出錯誤提示
          btn.textContent = '❌';                // 顯示錯誤圖示
        }

        // 1秒後還原按鈕狀態
        setTimeout(() => {
          btn.textContent = '📸';
          btn.style.pointerEvents = 'auto';
        }, 1000);
      });

      btnGroup.appendChild(btn);   // 將按鈕加入按鈕群組
    });
  });

  // 監聽整個 body 的 DOM 變化
  observer.observe(document.body, { childList: true, subtree: true });

})();