// ==UserScript==
// @name Floating Screenshot Button for Facebook Posts
// @name:zh-TW FaceBook 貼文懸浮截圖按鈕
// @name:zh-CN FaceBook 贴文悬浮截图按钮
// @namespace http://tampermonkey.net/
// @version 3.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);
// ===== 截圖參數 =====
const screenshotOptions = {
backgroundColor: '#1c1c1d',
pixelRatio: 2,
cacheBust: true
};
// ===== 補零 =====
const pad = n => n.toString().padStart(2, '0');
// ===== 從貼文中取得 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 { }
}
const dataFt = post.getAttribute('data-ft');
if (dataFt) {
const match = dataFt.match(/"top_level_post_id":"(\d+)"/);
if (match) return match[1];
}
try {
const url = new URL(window.location.href);
const fbid = url.searchParams.get('fbid') || url.searchParams.get('story_fbid');
if (fbid) return fbid;
} catch { }
return 'unknownFBID';
}
// ===== 建立截圖按鈕 =====
function createScreenshotButton(post, filenameBuilder) {
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';
// 用於儲存原始 margin-top
const originalMargins = [];
try {
// 展開「查看更多」
post.querySelectorAll('span,a,div,button').forEach(el => {
const txt = el.innerText?.trim() || el.textContent?.trim();
if (['查看更多', '顯示更多', 'See more', 'See More', '…更多'].includes(txt)) {
el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}
});
await new Promise(r => setTimeout(r, 800));
// 暫時調整內文的 margin-top,避免文字被頭像遮擋
const storyMessages = post.querySelectorAll('div[dir="auto"], div[data-ad-preview="message"]');
storyMessages.forEach(el => {
const computedMargin = window.getComputedStyle(el).marginTop;
originalMargins.push({ el, margin: computedMargin });
el.style.marginTop = '20px';
});
await new Promise(r => setTimeout(r, 100));
const filename = filenameBuilder();
await document.fonts.ready;
const dataUrl = await window.htmlToImage.toPng(post, screenshotOptions);
const link = document.createElement('a');
link.href = dataUrl;
link.download = filename;
link.click();
btn.textContent = '✅';
} catch (err) {
console.error('截圖錯誤:', err);
alert('截圖失敗,請稍後再試');
btn.textContent = '❌';
} finally {
// 還原原本的 margin-top
originalMargins.forEach(({ el, margin }) => {
el.style.marginTop = margin;
});
}
setTimeout(() => {
btn.textContent = '📸';
btn.style.pointerEvents = 'auto';
}, 1000);
});
return btn;
}
// ===== 判斷頁面類型 =====
function getPageType(path) {
if (path.startsWith('/groups/')) return 'group';
const segments = path.split('/').filter(Boolean);
const excluded = ['watch', 'gaming', 'marketplace', 'groups', 'friends', 'notifications', 'messages'];
if (segments.length > 0 && !excluded.includes(segments[0])) return 'page';
return 'home';
}
// ===== 核心觀察器 =====
const observer = new MutationObserver(() => {
const type = getPageType(location.pathname);
if (type === 'home') {
document.querySelectorAll('div.x1lliihq').forEach(post => {
if (post.dataset.sbtn === '1') return;
const textContent = post.innerText || post.textContent || '';
if (textContent.includes('社團建議') || textContent.includes('Suggested Groups')) 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 fbid = getFbidFromPost(post);
btnGroup.appendChild(createScreenshotButton(post, () => {
const now = new Date();
return `${fbid}_${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}_${pad(now.getMinutes())}_${pad(now.getSeconds())}.png`;
}));
});
}
if (type === 'group' || type === 'page') {
document.querySelectorAll('div.x1yztbdb').forEach(post => {
if (post.dataset.sbtn === '1') return;
let btnParent = post.querySelector('div.xqcrz7y') || post.closest('div.xqcrz7y');
if (!btnParent) return;
post.dataset.sbtn = '1';
btnParent.style.position = 'relative';
btnParent.appendChild(createScreenshotButton(post, () => {
const now = new Date();
if (type === 'group') {
const groupId = location.pathname.match(/^\/groups\/(\d+)/)?.[1] || 'unknownGroup';
return `${groupId}_${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}_${pad(now.getMinutes())}_${pad(now.getSeconds())}.png`;
} else {
const pageName = location.pathname.split('/').filter(Boolean)[0] || 'page';
return `${pageName}_${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}_${pad(now.getMinutes())}_${pad(now.getSeconds())}.png`;
}
}));
});
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();