您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在贴文右上新增一个悬浮截图按钮,按下后可以对贴文进行截图保存,方便与其他人分享
当前为
// ==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); })();