Files
2025-11-11 10:36:31 +01:00

1129 lines
32 KiB
JavaScript

// Facebook Post Tracker Extension
// Uses API_BASE_URL from config.js
const EXTENSION_VERSION = '1.1.0';
const PROCESSED_ATTR = 'data-fb-tracker-processed';
const API_URL = `${API_BASE_URL}/api`;
console.log(`[FB Tracker v${EXTENSION_VERSION}] Extension loaded, API URL:`, API_URL);
// Profile state helpers
async function fetchBackendProfileNumber() {
try {
const response = await fetch(`${API_URL}/profile-state`);
if (!response.ok) {
return null;
}
const data = await response.json();
if (data && data.profile_number) {
return data.profile_number;
}
} catch (error) {
console.warn('[FB Tracker] Failed to fetch profile state from backend:', error);
}
return null;
}
function storeProfileNumberLocally(profileNumber) {
chrome.storage.sync.set({ profileNumber });
}
async function getProfileNumber() {
const backendProfile = await fetchBackendProfileNumber();
if (backendProfile) {
storeProfileNumberLocally(backendProfile);
console.log('[FB Tracker] Profile number (backend):', backendProfile);
return backendProfile;
}
return new Promise((resolve) => {
chrome.storage.sync.get(['profileNumber'], (result) => {
const profile = result.profileNumber || 1;
console.log('[FB Tracker] Profile number (local):', profile);
resolve(profile);
});
});
}
// Extract post URL from post element
function cleanPostUrl(rawUrl) {
if (!rawUrl) {
return '';
}
const cftIndex = rawUrl.indexOf('__cft__');
let trimmed = cftIndex !== -1 ? rawUrl.slice(0, cftIndex) : rawUrl;
trimmed = trimmed.replace(/[?&]$/, '');
return trimmed;
}
function toAbsoluteFacebookUrl(rawUrl) {
if (!rawUrl) {
return null;
}
const cleaned = cleanPostUrl(rawUrl);
let url;
try {
url = new URL(cleaned);
} catch (error) {
try {
url = new URL(cleaned, window.location.origin);
} catch (innerError) {
return null;
}
}
const host = url.hostname.toLowerCase();
if (!host.endsWith('facebook.com')) {
return null;
}
return url;
}
function isValidFacebookPostUrl(url) {
if (!url) {
return false;
}
const path = url.pathname.toLowerCase();
const searchParams = url.searchParams;
const postPathPatterns = [
'/posts/',
'/permalink/',
'/photos/',
'/videos/',
'/reel/'
];
if (postPathPatterns.some(pattern => path.includes(pattern))) {
return true;
}
if (path.startsWith('/photo') && searchParams.has('fbid')) {
return true;
}
if (path.startsWith('/photo.php') && searchParams.has('fbid')) {
return true;
}
return false;
}
function formatFacebookPostUrl(url) {
if (!url) {
return '';
}
let search = url.search;
if (search.endsWith('?') || search.endsWith('&')) {
search = search.slice(0, -1);
}
return `${url.origin}${url.pathname}${search}`;
}
function extractPostUrlCandidate(rawUrl) {
const absoluteUrl = toAbsoluteFacebookUrl(rawUrl);
if (!absoluteUrl || !isValidFacebookPostUrl(absoluteUrl)) {
return '';
}
return formatFacebookPostUrl(absoluteUrl);
}
function getPostUrl(postElement) {
console.log('[FB Tracker] Extracting URL from post element');
const attributionLinks = postElement.querySelectorAll('a[attributionsrc*="/privacy_sandbox/comet/"][href]');
for (const link of attributionLinks) {
const candidate = extractPostUrlCandidate(link.href);
if (candidate) {
console.log('[FB Tracker] Found post URL via attribution link:', candidate);
return candidate;
}
}
const links = postElement.querySelectorAll('a[href]');
for (const link of links) {
const candidate = extractPostUrlCandidate(link.href);
if (candidate) {
console.log('[FB Tracker] Found post URL via fallback patterns:', candidate);
return candidate;
}
}
const fallbackCandidate = extractPostUrlCandidate(window.location.href);
if (fallbackCandidate) {
console.log('[FB Tracker] Using fallback URL:', fallbackCandidate);
return fallbackCandidate;
}
console.log('[FB Tracker] No valid post URL found');
return '';
}
// Check if post is already tracked
async function checkPostStatus(postUrl) {
try {
console.log('[FB Tracker] Checking post status for:', postUrl);
const response = await fetch(`${API_URL}/posts/by-url?url=${encodeURIComponent(postUrl)}`);
if (response.ok) {
const data = await response.json();
console.log('[FB Tracker] Post status:', data);
return data;
}
console.log('[FB Tracker] Post not tracked yet');
return null;
} catch (error) {
console.error('[FB Tracker] Error checking post status:', error);
return null;
}
}
// Add post to tracking
async function markPostChecked(postId, profileNumber) {
try {
console.log('[FB Tracker] Marking post as checked:', postId, 'Profile:', profileNumber);
const response = await fetch(`${API_URL}/posts/${postId}/check`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ profile_number: profileNumber })
});
if (response.ok) {
const data = await response.json();
console.log('[FB Tracker] Post marked as checked:', data);
return data;
}
if (response.status === 409) {
console.log('[FB Tracker] Post already checked by this profile');
return null;
}
console.error('[FB Tracker] Failed to mark post as checked:', response.status);
return null;
} catch (error) {
console.error('[FB Tracker] Error marking post as checked:', error);
return null;
}
}
async function addPostToTracking(postUrl, targetCount, profileNumber, options = {}) {
try {
console.log('[FB Tracker] Adding post:', postUrl, 'Target:', targetCount, 'Profile:', profileNumber);
const response = await fetch(`${API_URL}/posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: postUrl,
target_count: targetCount,
profile_number: profileNumber
})
});
if (response.ok) {
const data = await response.json();
console.log('[FB Tracker] Post added successfully:', data);
if (data && data.id) {
const checkedData = await markPostChecked(data.id, profileNumber);
await captureAndUploadScreenshot(data.id, options.postElement || null);
if (checkedData) {
return checkedData;
}
}
return data;
} else {
console.error('[FB Tracker] Failed to add post:', response.status);
return null;
}
} catch (error) {
console.error('[FB Tracker] Error adding post:', error);
return null;
}
}
function normalizeButtonLabel(button) {
const aria = button.getAttribute('aria-label');
if (aria) {
return aria.trim().toLowerCase();
}
const title = button.getAttribute('title');
if (title) {
return title.trim().toLowerCase();
}
return (button.textContent || '').trim().toLowerCase();
}
const LIKE_LABEL_KEYWORDS = ['gefällt mir', 'like', 'mag ich'];
const COMMENT_LABEL_KEYWORDS = ['kommentieren', 'comment', 'kommentar', 'antwort hinterlassen'];
const SHARE_LABEL_KEYWORDS = ['teilen', 'share', 'senden', 'posten', 'profil posten'];
const REPLY_LABEL_KEYWORDS = ['antworten', 'reply'];
const LIKE_ROLE_KEYWORDS = ['gefällt mir', 'like'];
const COMMENT_ROLE_KEYWORDS = ['comment'];
const SHARE_ROLE_KEYWORDS = ['share'];
const REPLY_ROLE_KEYWORDS = ['reply'];
function matchesKeyword(label, keywords) {
return keywords.some((keyword) => label.includes(keyword));
}
function collectButtonMeta(button) {
const textParts = [];
const roleParts = [];
const label = normalizeButtonLabel(button);
if (label) {
textParts.push(label);
}
const collectRole = (value) => {
if (!value) {
return;
}
const lower = value.toLowerCase();
roleParts.push(lower);
const tokens = lower.split(/[_\-\s]+/);
tokens.forEach((token) => {
if (token) {
roleParts.push(token);
}
});
};
collectRole(button.getAttribute('data-ad-rendering-role'));
const descendantRoles = button.querySelectorAll('[data-ad-rendering-role]');
descendantRoles.forEach((el) => {
collectRole(el.getAttribute('data-ad-rendering-role'));
if (el.textContent) {
textParts.push(normalizeButtonLabel(el));
}
});
return {
text: textParts.join(' '),
roles: roleParts.join(' ')
};
}
function buttonClassification(button) {
const meta = collectButtonMeta(button);
const text = meta.text;
const roles = meta.roles;
const combined = `${text} ${roles}`;
return {
isLike: matchesKeyword(combined, LIKE_LABEL_KEYWORDS) || matchesKeyword(roles, LIKE_ROLE_KEYWORDS),
isComment: matchesKeyword(combined, COMMENT_LABEL_KEYWORDS) || matchesKeyword(roles, COMMENT_ROLE_KEYWORDS),
isShare: matchesKeyword(combined, SHARE_LABEL_KEYWORDS) || matchesKeyword(roles, SHARE_ROLE_KEYWORDS),
isReply: matchesKeyword(combined, REPLY_LABEL_KEYWORDS) || matchesKeyword(roles, REPLY_ROLE_KEYWORDS)
};
}
function findShareButtonForArticle(article) {
const direct = article.querySelector('[data-ad-rendering-role="share_button"]');
if (direct) {
return direct;
}
const shareButtons = document.querySelectorAll('[data-ad-rendering-role="share_button"]');
for (const button of shareButtons) {
const owningArticle = button.closest('[role="article"]');
if (owningArticle === article) {
return button;
}
}
return null;
}
function hasInteractionButtons(container) {
if (!container) {
return false;
}
const buttons = container.querySelectorAll('[role="button"], button');
if (!buttons.length) {
return false;
}
let hasLike = false;
let hasComment = false;
let hasShare = false;
let hasReply = false;
for (const button of buttons) {
const info = buttonClassification(button);
if (info.isLike) {
hasLike = true;
}
if (info.isComment) {
hasComment = true;
}
if (info.isShare) {
hasShare = true;
}
if (info.isReply) {
hasReply = true;
}
}
if (hasLike || hasComment || hasShare) {
console.log('[FB Tracker] Container analysis', {
tag: container.tagName,
classes: container.className,
hasLike,
hasComment,
hasShare,
hasReply,
buttonCount: buttons.length
});
}
if (!hasLike || !hasComment) {
return false;
}
if (hasShare) {
return true;
}
return !hasReply;
}
function findButtonBar(postElement) {
const shareAnchor = findShareButtonForArticle(postElement);
if (shareAnchor) {
let container = shareAnchor.closest('[role="button"]') || shareAnchor.closest('button');
for (let i = 0; i < 4 && container; i++) {
if (hasInteractionButtons(container)) {
console.log('[FB Tracker] Found button bar via share anchor');
return container;
}
container = container.parentElement;
}
}
// Gather all accessible buttons inside the article
const buttons = Array.from(postElement.querySelectorAll('[role="button"], button'));
const interactionButtons = buttons.filter((button) => {
const info = buttonClassification(button);
return info.isLike || info.isComment || info.isShare;
});
const anchorElements = [
postElement.querySelector('[data-ad-rendering-role="share_button"]'),
postElement.querySelector('[data-ad-rendering-role="comment_button"]'),
postElement.querySelector('[data-ad-rendering-role*="gefällt"]')
].filter(Boolean);
const candidates = new Set(interactionButtons);
anchorElements.forEach((el) => {
const buttonContainer = el.closest('[role="button"]') || el.closest('button');
if (buttonContainer) {
candidates.add(buttonContainer);
}
});
const seen = new Set();
for (const button of candidates) {
const info = buttonClassification(button);
console.log('[FB Tracker] Candidate button', {
label: normalizeButtonLabel(button),
hasLike: info.isLike,
hasComment: info.isComment,
hasShare: info.isShare,
hasReply: info.isReply,
classes: button.className
});
let current = button;
while (current && current !== postElement && current !== document.body) {
if (seen.has(current)) {
current = current.parentElement;
continue;
}
seen.add(current);
const hasBar = hasInteractionButtons(current);
if (hasBar) {
console.log('[FB Tracker] Found button bar');
return current;
}
current = current.parentElement;
}
}
let sibling = postElement.nextElementSibling;
for (let i = 0; i < 6 && sibling; i++) {
if (!seen.has(sibling) && hasInteractionButtons(sibling)) {
console.log('[FB Tracker] Found button bar via next sibling');
return sibling;
}
sibling = sibling.nextElementSibling;
}
sibling = postElement.previousElementSibling;
for (let i = 0; i < 3 && sibling; i++) {
if (!seen.has(sibling) && hasInteractionButtons(sibling)) {
console.log('[FB Tracker] Found button bar via previous sibling');
return sibling;
}
sibling = sibling.previousElementSibling;
}
let parent = postElement.parentElement;
for (let depth = 0; depth < 4 && parent; depth++) {
if (!seen.has(parent) && hasInteractionButtons(parent)) {
console.log('[FB Tracker] Found button bar via parent');
return parent;
}
let next = parent.nextElementSibling;
for (let i = 0; i < 4 && next; i++) {
if (!seen.has(next) && hasInteractionButtons(next)) {
console.log('[FB Tracker] Found button bar via parent sibling');
return next;
}
next = next.nextElementSibling;
}
let prev = parent.previousElementSibling;
for (let i = 0; i < 3 && prev; i++) {
if (!seen.has(prev) && hasInteractionButtons(prev)) {
console.log('[FB Tracker] Found button bar via parent previous sibling');
return prev;
}
prev = prev.previousElementSibling;
}
parent = parent.parentElement;
}
console.log('[FB Tracker] Button bar not found');
return null;
}
function findPostContainers() {
const containers = [];
const seen = new Set();
const likeButtons = document.querySelectorAll('[data-ad-rendering-role="gefällt mir_button"], [data-ad-rendering-role*="gefällt" i]');
likeButtons.forEach((likeButton) => {
const container = likeButton.closest('div[aria-posinset]');
if (!container) {
return;
}
if (container.getAttribute(PROCESSED_ATTR) === '1') {
return;
}
if (container.querySelector('.fb-tracker-ui')) {
return;
}
const commentButton = container.querySelector('[data-ad-rendering-role="comment_button"], [data-ad-rendering-role*="comment" i]');
const shareButton = container.querySelector('[data-ad-rendering-role="share_button"], [data-ad-rendering-role*="share" i], [data-ad-rendering-role*="teilen" i]');
if (!commentButton || !shareButton) {
return;
}
if (seen.has(container)) {
return;
}
seen.add(container);
containers.push({ container, likeButton, commentButton, shareButton });
});
return containers;
}
function captureScreenshot(screenshotRect) {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ type: 'captureScreenshot', screenshotRect }, (response) => {
if (chrome.runtime.lastError) {
console.warn('[FB Tracker] Screenshot capture failed:', chrome.runtime.lastError.message);
resolve(null);
return;
}
if (!response || response.error) {
if (response && response.error) {
console.warn('[FB Tracker] Screenshot capture reported error:', response.error);
}
resolve(null);
return;
}
if (!response.imageData) {
console.warn('[FB Tracker] Screenshot capture returned no data');
resolve(null);
return;
}
(async () => {
try {
let result = response.imageData;
if (screenshotRect) {
const cropped = await cropScreenshot(result, screenshotRect);
if (cropped) {
result = cropped;
}
}
resolve(result);
} catch (error) {
console.warn('[FB Tracker] Screenshot processing failed:', error);
resolve(response.imageData);
}
})();
});
});
}
async function uploadScreenshot(postId, imageData) {
try {
const response = await fetch(`${API_URL}/posts/${postId}/screenshot`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ imageData })
});
if (!response.ok) {
console.warn('[FB Tracker] Screenshot upload failed:', response.status);
}
} catch (error) {
console.error('[FB Tracker] Screenshot upload error:', error);
}
}
async function captureAndUploadScreenshot(postId, postElement) {
const imageData = await captureElementScreenshot(postElement);
if (!imageData) {
return;
}
const optimized = await maybeDownscaleScreenshot(imageData);
await uploadScreenshot(postId, optimized);
}
async function captureElementScreenshot(element) {
if (!element) {
return await captureScreenshot();
}
const horizontalMargin = 32;
const verticalMargin = 96;
const maxSegments = 12;
const delayBetweenScrolls = 200;
const originalScrollX = window.scrollX;
const originalScrollY = window.scrollY;
const devicePixelRatio = window.devicePixelRatio || 1;
const stickyOffset = getStickyHeaderHeight();
const segments = [];
const elementRect = element.getBoundingClientRect();
const elementTop = elementRect.top + window.scrollY;
const elementBottom = elementRect.bottom + window.scrollY;
const documentHeight = document.documentElement.scrollHeight;
const startY = Math.max(0, elementTop - verticalMargin - stickyOffset);
const endY = Math.min(documentHeight, elementBottom + verticalMargin);
const baseDocTop = Math.max(0, elementTop - verticalMargin);
try {
let iteration = 0;
let targetScroll = startY;
while (iteration < maxSegments) {
iteration += 1;
window.scrollTo({ top: targetScroll, left: window.scrollX, behavior: 'auto' });
await delay(delayBetweenScrolls);
const rect = element.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const captureTop = Math.max(0, rect.top - verticalMargin - stickyOffset);
const captureBottom = Math.min(viewportHeight, rect.bottom + verticalMargin);
const captureHeight = captureBottom - captureTop;
if (captureHeight <= 0) {
break;
}
const captureRect = {
left: rect.left - horizontalMargin,
top: captureTop,
width: rect.width + horizontalMargin * 2,
height: captureHeight,
devicePixelRatio
};
const segmentData = await captureScreenshot(captureRect);
if (!segmentData) {
break;
}
const docTop = Math.max(0, window.scrollY + captureTop);
const docBottom = docTop + captureHeight;
segments.push({ data: segmentData, docTop, docBottom });
const reachedBottom = docBottom >= endY - 4;
if (reachedBottom) {
break;
}
const nextScroll = docBottom - Math.max(0, (viewportHeight - stickyOffset) * 0.5);
const maxScroll = Math.max(0, endY - viewportHeight);
targetScroll = Math.min(nextScroll, maxScroll);
if (targetScroll <= window.scrollY + 1) {
targetScroll = window.scrollY + Math.max(160, viewportHeight * 0.6);
}
if (targetScroll <= window.scrollY + 1 || targetScroll >= endY) {
break;
}
}
} finally {
window.scrollTo({ top: originalScrollY, left: originalScrollX, behavior: 'auto' });
}
if (!segments.length) {
return await captureScreenshot();
}
const stitched = await stitchScreenshotSegments(segments, devicePixelRatio, baseDocTop);
return stitched;
}
async function stitchScreenshotSegments(segments, devicePixelRatio, baseDocTop) {
const images = [];
let maxDocBottom = baseDocTop;
for (const segment of segments) {
const img = await loadImage(segment.data);
if (!img) {
continue;
}
images.push({ img, docTop: segment.docTop, docBottom: segment.docBottom });
if (segment.docBottom > maxDocBottom) {
maxDocBottom = segment.docBottom;
}
}
if (!images.length) {
return null;
}
const width = images.reduce((max, item) => Math.max(max, item.img.width), 0);
const totalHeightPx = Math.max(1, Math.round((maxDocBottom - baseDocTop) * devicePixelRatio));
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = totalHeightPx;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, totalHeightPx);
for (const { img, docTop } of images) {
const offsetY = Math.round(Math.max(0, docTop - baseDocTop) * devicePixelRatio);
ctx.drawImage(img, 0, offsetY);
}
return canvas.toDataURL('image/jpeg', 0.85);
}
async function cropScreenshot(imageData, rect) {
if (!rect) {
return imageData;
}
try {
const image = await loadImage(imageData);
if (!image) {
return imageData;
}
const ratio = rect.devicePixelRatio || window.devicePixelRatio || 1;
const rawLeft = (rect.left || 0) * ratio;
const rawTop = (rect.top || 0) * ratio;
const rawWidth = (rect.width || image.width) * ratio;
const rawHeight = (rect.height || image.height) * ratio;
const rawRight = rawLeft + rawWidth;
const rawBottom = rawTop + rawHeight;
const left = Math.max(0, Math.floor(rawLeft));
const top = Math.max(0, Math.floor(rawTop));
const right = Math.min(image.width, Math.ceil(rawRight));
const bottom = Math.min(image.height, Math.ceil(rawBottom));
const width = Math.max(1, right - left);
const height = Math.max(1, bottom - top);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, left, top, width, height, 0, 0, width, height);
return canvas.toDataURL('image/jpeg', 0.85);
} catch (error) {
console.warn('[FB Tracker] Failed to crop screenshot:', error);
return imageData;
}
}
async function maybeDownscaleScreenshot(imageData) {
try {
const maxWidth = 1600;
const current = await loadImage(imageData);
if (!current) {
return imageData;
}
if (current.width <= maxWidth) {
return imageData;
}
const scale = maxWidth / current.width;
const canvas = document.createElement('canvas');
canvas.width = Math.round(current.width * scale);
canvas.height = Math.round(current.height * scale);
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(current, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL('image/jpeg', 0.8);
} catch (error) {
console.warn('[FB Tracker] Failed to downscale screenshot:', error);
return imageData;
}
}
function getStickyHeaderHeight() {
try {
const banner = document.querySelector('[role="banner"], header[role="banner"]');
if (!banner) {
return 0;
}
const rect = banner.getBoundingClientRect();
if (!rect || !rect.height) {
return 0;
}
return Math.min(rect.height, 160);
} catch (error) {
console.warn('[FB Tracker] Failed to determine sticky header height:', error);
return 0;
}
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function loadImage(dataUrl) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => resolve(null);
img.src = dataUrl;
});
}
// Create the tracking UI
async function createTrackerUI(postElement, buttonBar) {
// Check if UI already exists
if (postElement.querySelector('.fb-tracker-ui')) {
console.log('[FB Tracker] UI already exists for this post');
return;
}
const postUrl = getPostUrl(postElement);
if (!postUrl) {
console.log('[FB Tracker] No URL found for post');
return;
}
console.log('[FB Tracker] Creating tracker UI for:', postUrl);
// Create UI container
const container = document.createElement('div');
container.className = 'fb-tracker-ui';
container.style.cssText = `
padding: 12px 16px;
background-color: #f0f2f5;
border-top: 1px solid #e4e6eb;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
`;
// Check current status
const profileNumber = await getProfileNumber();
const postData = await checkPostStatus(postUrl);
if (postData) {
const checkedCount = postData.checked_count ?? (Array.isArray(postData.checks) ? postData.checks.length : 0);
const statusText = `${checkedCount}/${postData.target_count}`;
const completed = checkedCount >= postData.target_count;
const lastCheck = Array.isArray(postData.checks) && postData.checks.length
? new Date(postData.checks[postData.checks.length - 1].checked_at).toLocaleString('de-DE')
: null;
container.innerHTML = `
<div style="color: #65676b;">
<strong>Tracker:</strong> ${statusText}${completed ? ' ✓ Ziel erreicht' : ''}
</div>
${lastCheck ? `<div style="color: #65676b;">Letzte Bestätigung: ${lastCheck}</div>` : ''}
`;
console.log('[FB Tracker] Showing status:', statusText);
} else {
// Post not tracked - show add UI
const selectId = `tracker-select-${Date.now()}`;
container.innerHTML = `
<label for="${selectId}" style="color: #65676b; font-weight: 500;">
Zum Tracker hinzufügen:
</label>
<select id="${selectId}" style="
padding: 6px 10px;
border: 1px solid #ccd0d5;
border-radius: 6px;
background: white;
font-size: 14px;
cursor: pointer;
">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
<button class="fb-tracker-add-btn" style="
padding: 6px 16px;
background-color: #1877f2;
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
">
Hinzufügen
</button>
`;
// Add click handler for the button
const addButton = container.querySelector('.fb-tracker-add-btn');
const selectElement = container.querySelector('select');
selectElement.value = '2';
addButton.addEventListener('click', async () => {
const targetCount = parseInt(selectElement.value, 10);
console.log('[FB Tracker] Add button clicked, target:', targetCount);
addButton.disabled = true;
addButton.textContent = 'Wird hinzugefügt...';
postElement.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'nearest' });
await delay(220);
const result = await addPostToTracking(postUrl, targetCount, profileNumber, { postElement });
if (result) {
const checks = Array.isArray(result.checks) ? result.checks : [];
const checkedCount = typeof result.checked_count === 'number' ? result.checked_count : checks.length;
const targetTotal = result.target_count || targetCount;
const statusText = `${checkedCount}/${targetTotal}`;
const completed = checkedCount >= targetTotal;
const lastCheck = checks.length ? new Date(checks[checks.length - 1].checked_at).toLocaleString('de-DE') : null;
let statusHtml = `
<div style="color: #65676b;">
<strong>Tracker:</strong> ${statusText}${completed ? ' ✓ Ziel erreicht' : ' ✓ Erfolgreich hinzugefügt'}
</div>
`;
if (lastCheck) {
statusHtml += `
<div style="color: #65676b; margin-top: 6px;">
Letzte Bestätigung: ${lastCheck}
</div>
`;
}
container.innerHTML = statusHtml;
} else {
// Error
addButton.disabled = false;
addButton.textContent = 'Fehler - Erneut versuchen';
addButton.style.backgroundColor = '#e74c3c';
}
});
console.log('[FB Tracker] UI created for new post');
}
// Insert UI below the button bar if available, otherwise append at the end
if (buttonBar && buttonBar.parentElement) {
buttonBar.parentElement.insertBefore(container, buttonBar.nextSibling);
console.log('[FB Tracker] UI inserted after button bar');
} else {
postElement.appendChild(container);
console.log('[FB Tracker] UI inserted into article (fallback)');
}
}
// Check if article is a main post (not a comment)
function isMainPost(article, buttonBar) {
if (buttonBar) {
const shareIndicator = buttonBar.querySelector('[data-ad-rendering-role="share_button"]');
if (shareIndicator) {
return true;
}
}
// Comments are usually nested inside other articles or comment sections
// Check if this article is inside another article (likely a comment)
let parent = article.parentElement;
while (parent && parent !== document.body) {
if (parent.getAttribute('role') === 'article') {
// This article is inside another article - it's a comment
return false;
}
parent = parent.parentElement;
}
// Additional check: Main posts usually have a link with /posts/ or /permalink/
const postLinks = article.querySelectorAll('a[href*="/posts/"], a[href*="/permalink/"], a[href*="/photo"], a[href*="/videos/"], a[href*="/reel/"]');
if (postLinks.length === 0) {
if (article.querySelector('[data-ad-rendering-role="share_button"]')) {
return true;
}
const nextSibling = article.nextElementSibling;
if (nextSibling && nextSibling.querySelector('[data-ad-rendering-role="share_button"]')) {
const nextButtons = nextSibling.querySelectorAll('[role="button"]');
if (nextButtons.length > 30) {
return true;
}
}
// No post-type links found - likely a comment
return false;
}
return true;
}
// Find all Facebook posts on the page
function findPosts() {
console.log('[FB Tracker] Scanning for posts...');
const postContainers = findPostContainers();
console.log('[FB Tracker] Found', postContainers.length, 'candidate containers');
let processed = 0;
for (const { container } of postContainers) {
if (container.getAttribute(PROCESSED_ATTR) === '1') {
continue;
}
const buttonBar = findButtonBar(container);
if (!buttonBar) {
console.log('[FB Tracker] Skipping container without full button bar');
continue;
}
container.setAttribute(PROCESSED_ATTR, '1');
processed++;
console.log('[FB Tracker] Adding tracker to post #' + processed);
createTrackerUI(container, buttonBar);
}
console.log('[FB Tracker] Total processed posts:', processed);
}
// Initialize
console.log('[FB Tracker] Initializing...');
// Run multiple times to catch loading posts
setTimeout(findPosts, 2000);
setTimeout(findPosts, 4000);
setTimeout(findPosts, 6000);
// Debounced scan function
let scanTimeout = null;
function scheduleScan() {
if (scanTimeout) {
clearTimeout(scanTimeout);
}
scanTimeout = setTimeout(() => {
console.log('[FB Tracker] Scheduled scan triggered');
findPosts();
}, 1000);
}
// Watch for new posts being added to the page
const observer = new MutationObserver((mutations) => {
scheduleScan();
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
// Trigger scan on scroll (for infinite scroll)
let scrollTimeout = null;
window.addEventListener('scroll', () => {
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
scrollTimeout = setTimeout(() => {
console.log('[FB Tracker] Scroll detected, scanning...');
findPosts();
}, 1000);
});
console.log('[FB Tracker] Observer and scroll listener started');