FaceBook 貼文懸浮截圖按鈕

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

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

// ==UserScript==
// @name         Floating Screenshot Button for Facebook Posts
// @name:zh-TW   FaceBook 貼文懸浮截圖按鈕
// @name:zh-CN   FaceBook 贴文悬浮截图按钮
// @namespace    http://tampermonkey.net/
// @version      2.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://cdn.jsdelivr.net/npm/[email protected]/dist/html-to-image.min.js
// @license MIT
// ==/UserScript==
 
(function () {
  'use strict';
 
  // ===== 禁用聚焦樣式,移除藍框或陰影 =====
  const style = document.createElement('style');
  style.textContent = `
    *:focus, *:focus-visible, *:focus-within {
      outline: none !important;
      box-shadow: none !important;
    }
  `;
  document.head.appendChild(style);
 
  let lastRun = 0; // 上一次主頁按鈕建立的時間
  const debounceDelay = 1000; // 間隔時間(毫秒)
 
  // ===== 從貼文中取得 fbid(供檔名使用)=====
  function getFbidFromPost(post) {
    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');
        if (fbid) return fbid;
      } catch (e) { }
    }
    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) { }
    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';
  }
 
  // ===== 主頁貼文的截圖按鈕觀察器 =====
  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 {
          // 嘗試展開貼文內的「查看更多」
          const seeMoreCandidates = post.querySelectorAll('span, a, div, button');
          let clicked = false;
          for (const el of seeMoreCandidates) {
            const text = el.innerText?.trim() || el.textContent?.trim();
            if (!text) continue;
            if (text === '查看更多' || text === 'See more' || text === 'See More' || text === '…更多') {
              try {
                el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
                clicked = true;
                console.log('已點擊查看更多:', el);
              } catch (err) {
                console.warn('點擊查看更多失敗:', err);
              }
            }
          }
          if (clicked) await new Promise(r => setTimeout(r, 1000));
 
          // 滾動至貼文中央
          post.scrollIntoView({ behavior: 'smooth', block: 'center' });
          await new Promise(r => setTimeout(r, 500));
 
          const fbid = getFbidFromPost(post);
          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());
 
          // 執行截圖
          const dataUrl = await window.htmlToImage.toPng(post, {
            backgroundColor: '#1c1c1d',
            pixelRatio: 2,
            cacheBust: true,
          });
 
          // 儲存圖片
          const filename = `${fbid}_${datetimeStr}.png`;
          const link = document.createElement('a');
          link.href = dataUrl;
          link.download = filename;
          link.click();
 
          btn.textContent = '✅';
        } catch (err) {
          console.error('截圖錯誤:', err);
          alert('截圖失敗,請稍後再試');
          btn.textContent = '❌';
        }
 
        // 一秒後還原按鈕狀態
        setTimeout(() => {
          btn.textContent = '📸';
          btn.style.pointerEvents = 'auto';
        }, 1000);
      });
 
      btnGroup.appendChild(btn);
    });
  });
 
  observer.observe(document.body, { childList: true, subtree: true });
 
  // ====== 社團截圖功能:支援切換頁面、錯誤隔離、展開查看更多 =====
  let lastPathname = location.pathname;
  let groupObserver = null;
 
  function initGroupPostObserver() {
    if (groupObserver) return; // 如果已啟用觀察器,跳過
 
    groupObserver = new MutationObserver(() => {
      // 針對所有社團貼文容器尋找(CSS選擇器)
      document.querySelectorAll('div.x1yztbdb.x1n2onr6.xh8yej3.x1ja2u2z').forEach(post => {
        if (post.dataset.sbtn === '1') return; // 已經添加按鈕就跳過
 
        // 嘗試尋找合適的按鈕插入父元素
        let btnParent = post.querySelector('div.xqcrz7y.x78zum5.x1qx5ct2');
        if (!btnParent) {
          let p = post.parentElement;
          while (p && !p.classList.contains('xqcrz7y')) {
            if (p.querySelector('div.xqcrz7y.x78zum5.x1qx5ct2')) {
              btnParent = p.querySelector('div.xqcrz7y.x78zum5.x1qx5ct2');
              break;
            }
            p = p.parentElement;
          }
        }
        if (!btnParent) return; // 找不到按鈕插入點則跳過
 
        post.dataset.sbtn = '1'; // 標記該貼文已插入截圖按鈕
        btnParent.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 {
            // 自動展開貼文內的「查看更多」按鈕,確保內容完整
            const seeMoreCandidates = post.querySelectorAll('span, a, div, button');
            let clicked = false;
            for (const el of seeMoreCandidates) {
              const text = el.innerText?.trim() || el.textContent?.trim();
              if (!text) continue;
              if (text === '查看更多' || text === 'See more' || text === 'See More' || text === '…更多') {
                try {
                  el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
                  clicked = true;
                  console.log('已點擊查看更多:', el);
                } catch (err) {
                  console.warn('點擊查看更多失敗:', err);
                }
              }
            }
            if (clicked) await new Promise(r => setTimeout(r, 1000)); // 等待內容展開
 
            post.scrollIntoView({ behavior: 'smooth', block: 'center' });
            await new Promise(r => setTimeout(r, 500));
 
            // 取得社團ID,從網址 /groups/ 後面直接截取
            let groupId = 'unknownGroup';
            const match = location.pathname.match(/^\/groups\/(\d+)/);
            if (match) groupId = match[1];
 
            // 產生時間字串,格式 YYYYMMDD_HH_mm_ss
            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());
 
            // 進行截圖
            const dataUrl = await window.htmlToImage.toPng(post, {
              backgroundColor: '#1c1c1d',
              pixelRatio: 2,
              cacheBust: true,
            });
 
            // 下載檔案,檔名為 社團ID_時間.png
            const filename = `${groupId}_${datetimeStr}.png`;
            const link = document.createElement('a');
            link.href = dataUrl;
            link.download = filename;
            link.click();
 
            btn.textContent = '✅';
          } catch (err) {
            console.error('社團截圖錯誤:', err);
            alert('截圖失敗,請稍後再試');
            btn.textContent = '❌';
          }
 
          setTimeout(() => {
            btn.textContent = '📸';
            btn.style.pointerEvents = 'auto';
          }, 1000);
        });
 
        btnParent.appendChild(btn);
      });
    });
 
    groupObserver.observe(document.body, { childList: true, subtree: true });
    console.log('[腳本] 社團觀察器已啟動');
  }
 
  function stopGroupPostObserver() {
    if (groupObserver) {
      groupObserver.disconnect();
      groupObserver = null;
      console.log('[腳本] 社團觀察器已停止');
    }
  }
 
  // 初次載入時根據路徑啟用社團觀察器
  if (location.pathname.startsWith('/groups/')) {
    initGroupPostObserver();
  }
 
  // 每秒偵測路徑變更,確保切換頁面時啟用/關閉社團觀察器
  setInterval(() => {
    const currentPath = location.pathname;
    if (currentPath !== lastPathname) {
      lastPathname = currentPath;
      if (currentPath.startsWith('/groups/')) {
        initGroupPostObserver();
      } else {
        stopGroupPostObserver();
      }
    }
  }, 1000);
 
// ====== 粉絲專頁截圖按鈕(含展開查看更多與截圖邏輯)======
let pageObserver = null;
 
function initPagePostObserver() {
  if (pageObserver) return; // 避免重複建立觀察器
 
  pageObserver = new MutationObserver(() => {
    // 選取粉專貼文元素
    document.querySelectorAll('div.x1yztbdb.x1n2onr6.xh8yej3.x1ja2u2z').forEach(post => {
      if (post.dataset.sbtn === '1') return; // 已添加按鈕則跳過
 
      // 找到要放按鈕的父元素,透過 class 名尋找對應的按鈕容器
      let btnParent = post.querySelector('div.xqcrz7y.x78zum5.x1qx5ct2.x1y1aw1k.xf159sx.xwib8y2.xmzvs34.xw4jnvo');
      if (!btnParent) {
        // 往父層搜尋,確保可找到按鈕容器
        let p = post.parentElement;
        while (p && !p.classList.contains('xqcrz7y')) {
          if (p.querySelector('div.xqcrz7y.x78zum5.x1qx5ct2.x1y1aw1k.xf159sx.xwib8y2.xmzvs34.xw4jnvo')) {
            btnParent = p.querySelector('div.xqcrz7y.x78zum5.x1qx5ct2.x1y1aw1k.xf159sx.xwib8y2.xmzvs34.xw4jnvo');
            break;
          }
          p = p.parentElement;
        }
      }
      if (!btnParent) return; // 找不到則跳過
 
      post.dataset.sbtn = '1'; // 標記已添加按鈕
      btnParent.style.position = 'relative'; // 父元素設為 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 {
          // 展開「查看更多」按鈕,避免截圖內容被截斷
          const seeMoreCandidates = post.querySelectorAll('span, a, div, button');
          let clicked = false;
          for (const el of seeMoreCandidates) {
            const text = el.innerText?.trim() || el.textContent?.trim();
            if (!text) continue;
            if (text === '查看更多' || text === 'See more' || text === 'See More' || text === '…更多') {
              try {
                el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
                clicked = true;
                console.log('粉專:已點擊查看更多', el);
              } catch (err) {
                console.warn('粉專:點擊查看更多失敗', err);
              }
            }
          }
          if (clicked) await new Promise(r => setTimeout(r, 1000)); // 點擊後等待展開動畫完成
 
          // 滾動至貼文中央
          post.scrollIntoView({ behavior: 'smooth', block: 'center' });
          await new Promise(r => setTimeout(r, 500));
 
          // 取得時間字串
          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());
 
          // 取得粉專名稱(路徑的第一段)
          const pageName = location.pathname.split('/').filter(Boolean)[0] || 'page';
 
          // 使用 html-to-image 截圖貼文區塊
          const dataUrl = await window.htmlToImage.toPng(post, {
            backgroundColor: '#1c1c1d',
            pixelRatio: 2,
            cacheBust: true,
          });
 
          // 觸發下載
          const filename = `${pageName}_${datetimeStr}.png`;
          const link = document.createElement('a');
          link.href = dataUrl;
          link.download = filename;
          link.click();
 
          btn.textContent = '✅';
        } catch (err) {
          console.error('粉專截圖錯誤:', err);
          alert('截圖失敗,請稍後再試');
          btn.textContent = '❌';
        }
 
        setTimeout(() => {
          btn.textContent = '📸';
          btn.style.pointerEvents = 'auto';
        }, 1000);
      });
 
      btnParent.appendChild(btn);
    });
  });
 
  pageObserver.observe(document.body, { childList: true, subtree: true });
  console.log('[腳本] 粉專觀察器已啟動');
}
 
function stopPagePostObserver() {
  if (pageObserver) {
    pageObserver.disconnect();
    pageObserver = null;
    console.log('[腳本] 粉專觀察器已停止');
  }
}
 
// 初次載入啟用粉專觀察器
initPagePostObserver();
 
// 定時監控路徑變化,切換時重新啟用
setInterval(() => {
  const currentPath = location.pathname;
  if (currentPath !== lastPathname) {
    lastPathname = currentPath;
    initPagePostObserver();
  }
}, 1000);
 
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址