// ==UserScript==
// @name Floating Screenshot Button for Facebook Posts
// @name:zh-TW FaceBook 貼文懸浮截圖按鈕
// @name:zh-CN FaceBook 贴文悬浮截图按钮
// @namespace http://tampermonkey.net/
// @version 2.3
// @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(() => {
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 {
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 dataUrl = await window.htmlToImage.toPng(post, {
backgroundColor: '#1c1c1d',
pixelRatio: 2,
cacheBust: true,
});
const filename = `group_${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);
})();